Conversation
Replace the hardcoded, stale commit-scope list in CLAUDE.md section 3.3 with a pointer to the README Repo layout section, and finish the public-phase wording: drop the internal-AI-stats framing and the GitLab-era JIRA / merge-request residue from the trailer rules, the PR-open flow, and the PR template. Co-authored-by: Claude (claude-opus-4-8) <noreply@anthropic.com>
…e_and_template_cleanup docs: point commit-scope at README and finish public-phase cleanup
…adius Prompt injection can't be fully prevented, so this hardens defense-in-depth: label untrusted content with a tamper-resistant boundary the model is told to treat as data, and shrink the blast radius of a successful injection. Scope follows the analysis report — high-value, low-complexity items; deeper items (sandbox default flip, capability tokens, MCP/skill signing, memory audit log) are deliberately deferred. - security/trust.py: wrap_untrusted() fences external content with a per-call random nonce, so embedded fake close-markers can't escape the fence. - system prompt: a guideline telling the model to treat fenced/external content as data, watch for "ignore the above" / "you are now" style directives, and confirm high-impact actions via ask_user. - fence untrusted tool output at the single chokepoint (ContextBuilder.add_tool_result) and inside the subagent's own loop, plus the subagent result re-injected into the main agent. - fence recalled memory and the sentinel's memory/attention context as unverified; the planner is told unverified content must not drive privileged actions (spawn / nudge). - web_fetch: use security.network.validate_url_target (DNS-resolving SSRF check) instead of the previous scheme-only validator. - DirectExecutor: pass only an env allowlist (no host secrets) rather than the full host environment; warn loudly when running unsandboxed (backend=none). - subagents: per-session rolling-hour spawn rate limit (auto-recovering) to stop a prompt-injected re-injection loop without locking out legitimate use. Co-authored-by: Claude (claude-opus-4-8) <noreply@anthropic.com>
…-turns switch Wire the TUI /model command end to end: model.* RPC handlers (options / save_key / disconnect / add_model / remove_model); a config.set "model" branch that rebuilds the live provider and reassigns the agent loop so a switch takes effect on the next turn (guarded by is_turn_active); ProviderConfig.models for the manual picker catalogue; the custom provider routed through LiteLLM; and a base LLMProvider.chat_stream non-streaming fallback so non-litellm providers degrade gracefully instead of crashing. Frontend: the model picker gains an api_base input, inline model add/delete, and a structured switch through config.set; openrpc-derived types regenerated. Co-authored-by: Claude (claude-opus-4-8) <noreply@anthropic.com>
feat: add TUI /model — provider/model config CRUD and between-turns model switch
…ning_untrusted_content feat(security): label untrusted content and contain injection blast radius
Two related changes to the EverOS-backed memory subsystem. Migrated three recall improvements from the legacy everclaw repo (env vars renamed EVERCLAW_* -> RAVEN_*): - drop current-session hits from the # Memory recall so it does not duplicate the live conversation already in the history window (over-fetch top_k*2, then filter by session_id) - add `raven agent --message-file` for prompts beyond the shell ARG_MAX - env-gated episodic recall + preference annotation in the consolidator (RAVEN_EPISODIC_TOPK / RAVEN_ANNOTATE_PREFS / RAVEN_EPISODIC_CLUSTER), default-off Restructured when EverOS is invoked so native memory owns short-term and EverOS owns long-term: - recall is now on-miss: native runs every turn; EverOS user-track recall fires only when native surfaces nothing relevant to the query - the per-turn EverOS store is removed; EverOS consolidation runs only (a) intra-day when a session crosses everos_consolidation_threshold_pct of the context window (default 80, background, non-blocking) and (b) nightly at everos_nightly_consolidation_hour (default 0, offline) - backend.store gains force_flush to promote a batch immediately; consolidation is incremental per session (metadata watermark) so the two triggers never double-ingest - native short-term compaction (maybe_consolidate_by_tokens) is unchanged Tests: unit coverage for the threshold / nightly / on-miss paths plus a full in-memory lifecycle smoke, and a real-everos e2e for the force_flush single-store recall path. Co-authored-by: Claude (claude-opus-4-8) <noreply@anthropic.com>
EverOS defaulted both its user-level config toml and its data root (sqlite / lancedb / .index / ome.db) to a bare ~/.everos. Scope raven's instance under ~/.everos/raven so config and data stay isolated from any other EverOS consumer on the machine. Add update_everos.configure_everos_env(): it sets EVEROS_CONFIG_FILE and EVEROS_MEMORY__ROOT via setdefault (so an explicit operator override still wins) before EverOS's @cache-d load_settings() first runs. The everos backend factory and the understand_media tool factory call it as the earliest raven-controlled EverOS entry points; _EVEROS_CONFIG now derives from a single _EVEROS_BASE constant. No data migration: existing ~/.everos data is left untouched and a fresh ~/.everos/raven is created lazily on first write/use. Co-authored-by: Claude (claude-opus-4-8) <noreply@anthropic.com>
A fresh onboard now materializes the user-facing subset of the memory / plugins / skillForge extension blocks into ~/.raven/config.json so the knobs are discoverable and editable without reading the source. Seeding is setdefault-based (non-clobbering) and runs on every onboard, so it also backfills a pre-existing config that predates these blocks. - memory.backend defaults to "everos" (schema default); _memory_enabled now gates on an llm model being present, so a fresh modelless seed is treated as "not yet enabled" and the skip / non-interactive guard still resolves it to None when memory is opted out or left unconfigured. - skillForge.everos.enabled is seeded on; router.hub.endpoint is seeded to the shared Skill Hub URL with apiKey left null for the user to fill. - plugins.config["everos-memory"] is seeded with identity wiring that mirrors memory.userId / agentId so recall identities match. - internal-infra fields (embedding / reranker endpoints + their API keys, mass_library_db) are NOT written, so a real Bearer token never lands in a plaintext config; they stay at their schema defaults. Co-authored-by: Claude (claude-opus-4-8) <noreply@anthropic.com>
…lt_extension_blocks Feat/onboard default extension blocks
…recall_improvements feat: native short-term memory + everos long-term consolidation
…_memory_recall revert: pr EverMind-AI#9 native short-term memory + everos long-term consolidation
…readme_ci chore: prepare open source launch
## Summary - switch project metadata and license text to Apache-2.0 while retaining upstream notices - add public-ready community files: issue templates, SECURITY, CONTRIBUTING, CODE_OF_CONDUCT, Dependabot, and a stricter PR template - align README license copy with the public launch posture ## Repository settings updated via GitHub API - squash-only merge enabled; merge commit and rebase merge disabled - delete branch on merge and update branch enabled - Discussions enabled; Projects and Wiki disabled - Actions default workflow permissions set to read-only and PR self-approval disabled - description and homepage updated for Raven ## Verification - npm ci --ignore-scripts --no-audit --no-fund (root) - npm ci --ignore-scripts --no-audit --no-fund (ui-tui) - npm ci --ignore-scripts --no-audit --no-fund (bridge) - npx --no-install commitlint --from origin/main --to HEAD --config commitlint.config.cjs - PYTHONPATH=. python3 scripts/check_commit_messages.py origin/main..HEAD - uv run pre-commit run --from-ref origin/main --to-ref HEAD - git diff --check origin/main...HEAD ## Notes - GitHub branch protection/rulesets returned 403 while the repository is private on the current plan. Enable required checks after making the repository public. - Auto-merge remained disabled after the API patch, likely due repository/org availability or policy.
## Summary - add lightweight overview files for top-level repository sections that still displayed old non-conventional commit messages - update existing benchmark and demo READMEs with public-facing guidance - make the GitHub directory listing show a clean conventional commit message after merge without rewriting main history ## Verification - uv run pre-commit run --files LICENSES/README.md benchmarks/README.md demos/README.md docs/README.md raven/README.md tests/README.md - npx --no-install commitlint --from origin/main --to HEAD --config commitlint.config.cjs - PYTHONPATH=. python3 scripts/check_commit_messages.py origin/main..HEAD - uv run pre-commit run --from-ref origin/main --to-ref HEAD ## Note A normal pull request cannot rename historical commits already merged into main. This PR instead updates the affected directories with useful public-facing overview docs so GitHub displays the new conventional commit as the latest directory change.
Bumps [python-socketio](https://github.com/miguelgrinberg/python-socketio) from 5.16.1 to 5.16.2. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/miguelgrinberg/python-socketio/releases">python-socketio's releases</a>.</em></p> <blockquote> <h2>Release 5.16.2</h2> <p>See <a href="https://github.com/miguelgrinberg/python-socketio/blob/main/CHANGES.md">CHANGES.md</a> for release notes.</p> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/miguelgrinberg/python-socketio/blob/main/CHANGES.md">python-socketio's changelog</a>.</em></p> <blockquote> <h1>python-socketio change log</h1> <p><strong>Release 5.16.3</strong> - 2026-06-15</p> <ul> <li>Catch all exceptions in redis and rabbitmq client managers <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1581">#1581</a> (<a href="https://github.com/miguelgrinberg/python-socketio/commit/80bb5c9b07e6b53e45f610ec29a24124c539d41b">commit</a>)</li> </ul> <p><strong>Release 5.16.2</strong> - 2026-05-21</p> <ul> <li>Prevent unnecessary resource allocations <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1574">#1574</a> (<a href="https://github.com/miguelgrinberg/python-socketio/commit/ca140fe44d0ceb3004073645222abec182d8784b">commit</a>)</li> <li>Add zizmor to CI builds <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1570">#1570</a> (<a href="https://github.com/miguelgrinberg/python-socketio/commit/664dc27ec6f34179ed1724430eda3520627fc642">commit</a>)</li> </ul> <p><strong>Release 5.16.1</strong> - 2026-02-06</p> <ul> <li>Use configured JSON module in managers <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1549">#1549</a> (<a href="https://github.com/miguelgrinberg/python-socketio/commit/6229261ae6e8c01e675097242e333ee84587a544">commit</a>)</li> <li>Admin UI fixes: remove duplicate tasks, report transport upgrades (<a href="https://github.com/miguelgrinberg/python-socketio/commit/1c2eab13a92fac9e43663eb0b5f099eb1c40ea5b">commit</a>)</li> <li>Switch to Furo documentation template (<a href="https://github.com/miguelgrinberg/python-socketio/commit/add47d8c7abca697a2804141bbf29bfb095f7d5e">commit</a>)</li> <li>Add Python free-threading to CI <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1554">#1554</a> (<a href="https://github.com/miguelgrinberg/python-socketio/commit/ccdd2004a038ae4b8171a05120c5d0787332f7ee">commit</a>)</li> </ul> <p><strong>Release 5.16.0</strong> - 2025-12-24</p> <ul> <li>Address deprecation warnings (<a href="https://github.com/miguelgrinberg/python-socketio/commit/b235699d9b06564753c570b76055997e9d62a938">commit</a>)</li> <li>Drop Python 3.8 and 3.9 from CI builds (<a href="https://github.com/miguelgrinberg/python-socketio/commit/d0728d2f74538762dd551fa9cd0cd1fd5aedfa37">commit</a>)</li> </ul> <p><strong>Release 5.15.1</strong> - 2025-12-16</p> <ul> <li>Restore support multiple arguments via pubsub emits <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1540">#1540</a> (<a href="https://github.com/miguelgrinberg/python-socketio/commit/c279f26bb8c9887c4ca99d4d81ad331c4844438c">commit</a>)</li> </ul> <p><strong>Release 5.15.0</strong> - 2025-11-22</p> <ul> <li>Retry initial Redis connection <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1536">#1534</a> ([commit <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1">#1</a>](<a href="https://github.com/miguelgrinberg/python-socketio/commit/1e903e173a2d7b04599c4f7f9630c1abbb531fad">https://github.com/miguelgrinberg/python-socketio/commit/1e903e173a2d7b04599c4f7f9630c1abbb531fad</a>) [commit <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/2">#2</a>](<a href="https://github.com/miguelgrinberg/python-socketio/commit/5e898a9b93526e6e667767e54c60f4c84589989d">https://github.com/miguelgrinberg/python-socketio/commit/5e898a9b93526e6e667767e54c60f4c84589989d</a>))</li> <li>Correctly regenerate RabbitMQ binding after a connection failure <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1516">#1516</a> (<a href="https://github.com/miguelgrinberg/python-socketio/commit/c52e93b4a328d98a968bfbdec0cfd598b73ee913">commit</a>) (thanks <strong>Gritty_dev</strong>!)</li> <li>Support <code>ext_type</code> in the <code>MsgPackPacket</code> class <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1521">#1521</a> (<a href="https://github.com/miguelgrinberg/python-socketio/commit/208925344a48485d2cd56e40eb74266c3bcb5311">commit</a>)</li> <li>Support sending <code>bytesarray</code>s when using pub/sub managers (<a href="https://github.com/miguelgrinberg/python-socketio/commit/6c9b9974f72e2efdf62407ecab24ee6995448098">commit</a>)</li> <li>Fix typos in documentation <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1520">#1520</a> (<a href="https://github.com/miguelgrinberg/python-socketio/commit/db3f1c2a0105c30cb833ddfca8f05fe4320468fd">commit</a>) (thanks <strong>Lê Nam Khánh</strong>!)</li> <li>Improvements to the logging documentation (<a href="https://github.com/miguelgrinberg/python-socketio/commit/b423d0e38eef559b7e81acb7e32059de305f982c">commit</a>)</li> </ul> <p><strong>Release 5.14.3</strong> - 2025-10-29</p> <ul> <li>Support Python's native <code>ConnectionRefusedError</code> exception to reject a connection <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1515">#1515</a> (<a href="https://github.com/miguelgrinberg/python-socketio/commit/f3b18bde3f16437b223491d4c3e440ea37105fe3">commit</a>)</li> <li>Push binary data to the aiopika client manager <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1514">#1514</a> (<a href="https://github.com/miguelgrinberg/python-socketio/commit/194e1b7f277b5f72e1de78d3f614e7b8b6c788ac">commit</a>)</li> </ul> <p><strong>Release 5.14.2</strong> - 2025-10-15</p> <ul> <li>Restore binary message support in message queue setups <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1509">#1509</a> (<a href="https://github.com/miguelgrinberg/python-socketio/commit/bab4a10f48aaae11d7f832ebe5c30ad3f85d31b3">commit</a>)</li> <li>Fix formatting of client connection error <a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1507">#1507</a> (<a href="https://github.com/miguelgrinberg/python-socketio/commit/f298c9b54d76ab09ff72935937e1b9575bc45ffd">commit</a>)</li> <li>Add 3.14 and pypy-3.11 CI tasks (<a href="https://github.com/miguelgrinberg/python-socketio/commit/1f4cd3b025c294f25208ec3c05b5f8df6209e403">commit</a>)</li> <li>Improve documentation of the <code>BaseManager.get_participants()</code> method (<a href="https://github.com/miguelgrinberg/python-socketio/commit/33722a0d96036f005188b07b8b46a5ef091fe65f">commit</a>)</li> </ul> <p><strong>Release 5.14.1</strong> - 2025-10-02</p> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/miguelgrinberg/python-socketio/commit/6e2b7175c97c2785edd2ea60df026c12240c785c"><code>6e2b717</code></a> Release 5.16.2</li> <li><a href="https://github.com/miguelgrinberg/python-socketio/commit/cb658291085842372a16863d83e14373f85b3d3e"><code>cb65829</code></a> update python-engineio version</li> <li><a href="https://github.com/miguelgrinberg/python-socketio/commit/ca140fe44d0ceb3004073645222abec182d8784b"><code>ca140fe</code></a> prevent unnecessary resource allocation (<a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1574">#1574</a>)</li> <li><a href="https://github.com/miguelgrinberg/python-socketio/commit/b29beef6bfd38d5a3c3b89da96b7a4574a91efcf"><code>b29beef</code></a> tox configuration</li> <li><a href="https://github.com/miguelgrinberg/python-socketio/commit/e8981302a3d5b525ee73c2ec9c13859ce2f764e8"><code>e898130</code></a> Bump ujson from 5.4.0 to 5.12.1 in /examples/server/sanic (<a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1573">#1573</a>) #nolog</li> <li><a href="https://github.com/miguelgrinberg/python-socketio/commit/05c32f5da85fe5e5d647c937c7eab0972760ca05"><code>05c32f5</code></a> Bump qs and body-parser in /examples/server/javascript (<a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1572">#1572</a>) #nolog</li> <li><a href="https://github.com/miguelgrinberg/python-socketio/commit/287dc6715b801d319f5baa0fb727e232e728b8aa"><code>287dc67</code></a> Bump qs and body-parser in /examples/client/javascript (<a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1571">#1571</a>) #nolog</li> <li><a href="https://github.com/miguelgrinberg/python-socketio/commit/664dc27ec6f34179ed1724430eda3520627fc642"><code>664dc27</code></a> add zizmor to ci (<a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1570">#1570</a>)</li> <li><a href="https://github.com/miguelgrinberg/python-socketio/commit/14c62366a85cfe34e0bd69a22b905eb329873dd3"><code>14c6236</code></a> Bump django in /examples/server/wsgi/django_socketio (<a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1566">#1566</a>) #nolog</li> <li><a href="https://github.com/miguelgrinberg/python-socketio/commit/29b2e5cdbe87e8216bc46fccb864b84ab9f55cf9"><code>29b2e5c</code></a> Bump aiohttp from 3.13.3 to 3.13.4 in /examples/server/aiohttp (<a href="https://redirect.github.com/miguelgrinberg/python-socketio/issues/1565">#1565</a>) #nolog</li> <li>Additional commits viewable in <a href="https://github.com/miguelgrinberg/python-socketio/compare/v5.16.1...v5.16.2">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/EverMind-AI/Raven/network/alerts). </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
## Summary A read-only audit of all 298 test files found the suite in good shape -- exactly one genuinely fake test -- with the real debt sitting in file names and a scattering of assertions that passed for the wrong reason. This clears that, and fixes the bugs found while clearing it. **Three dead tests dealt with.** Two removed because neither could ever assert anything: - `test_chat_e2e.py` was a module-level skip over three `NotImplementedError` stubs. The wire-up it waited for has shipped (`tui_commands` instantiates `SubscriptionEmitter`) and the proposal doc it cited is gone. - `test_tui_exit_e2e.py` asserted that building the agent loop leaves lancedb's thread live. It does not: the loop is constructed but never started, and the memory plugin talks to everos over HTTP, so lancedb is not even imported. The assertion could not hold in any configuration. The third, `slashParity.test.ts`, shelled out to python for a `raven_cli.commands.COMMAND_REGISTRY` that no longer exists, and skipped all three of its cases when the import failed -- three skips on every run. The parity it wanted needs no python: `createSlashHandler.ts:45-51` routes a slash locally when `findSlashCommand` resolves it and otherwise hands it to `slash.exec`, a CLI subprocess a mutation cannot reach the live session from. It now asserts that predicate directly. `NATIVE_MUTATING_COMMANDS` and the three-way `classifyRoute` went with it: all six names in that set are also in the local registry, so the native branch could not change any outcome. **19 files renamed** off phase, ticket and bug codes (AGENTS.md section 5.1), including one reversed CLI name and two integration files moved onto a legal `<kind>`. Reference updates ride along: `.pre-commit-config.yaml` names one of these files in a `detect-private-key` exclude, and three docs pages point at them. **Assertions that passed for the wrong reason, tightened**: eight over-broad `pytest.raises(Exception)` narrowed to the type each site actually raises; an `except` clause that swallowed the `AssertionError` its own fakes use as a tripwire; a `hasattr` probe replaced by binding a broker and checking it stuck; and a recall assertion that read as a pass on an empty result set. **Dead weight removed**: ten unused locals, three dead code blocks, six `importorskip` guards on hard dependencies, two hardcoded `/tmp` paths, and ticket codes left in docstrings after the renames. **Two coverage gaps the deletions exposed are now closed**: - `tests/test_cli_exit.py` pins the hard-exit guard, which had no tests at all. - The TUI chat e2e gains a multi-turn case. It reads the persisted session rather than the screen, because a prompt is echoed into the transcript the moment it is typed -- screen-scraping for a planted word passes even when history is broken. **Three timing bugs fixed in the TUI e2e tests.** They keyed off the banner, which paints about seven seconds before the app accepts input, and submitted the moment text was typed. Dropped prompts made the pipeline look dead in one test, and made a negative assertion (`assert not leak_detected`) pass vacuously in another -- no prompt means no streaming means no leak. **Gaps in the two features that landed during review**, small enough to close here: the `session.py` wiring that fills `update_available` / `update_command` (removing it left the suite green), `_upgrade_command_works` (a fixture stubbed it to `True` everywhere), an `episodeSummary` case with an empty test body, and `clipToWidth` / `hasMeaningfulReasoning`, which had no coverage at all. The larger items from that audit are tracked in EverMind-AI#229. **Upstream review attributions dropped from ui-tui comments.** 36 mentions of another project's review thread (#19835 as "Copilot round-N review"), plus #18994, #19194 and #14045 -- none resolve to anything here, and AGENTS.md section 1.1 rules out comments pointing at information only visible elsewhere. The rationale each carried is kept; four `describe()` titles lose a trailing `(#18994)`. Three went further and documented a python counterpart that does not exist: `platform.ts` credited key-spelling normalization to `raven_cli/voice.py` (no such module -- `raven/tui_rpc/methods/_stubs.py` answers `voice.toggle` with "voice not supported in Raven v0.1"), and `useConfigSync.ts` plus its test credited an `interrupt` default to `raven_cli/config.py` and `tui_gateway/server.py::_load_busy_input_mode` (`busy_input_mode` appears nowhere in python; neither module exists). **One flaky test fixed, caught by this PR's own CI run.** `test_stdio_no_executor_does_not_raise` set `command="true"` and let `connect_mcp_servers` really spawn it, so the result depended on how a live child process raced the MCP handshake -- it failed once on CI in the full-suite run while passing 30 out of 30 standalone runs locally. It also asserted only that whatever came back was not a `SandboxInitError`. The transport now raises instead of running (the idiom its sibling test already uses) and the test asserts the stdio branch was reached. Review follow-ups, folded in: the hard-exit test now skips rather than asserting no lancedb thread exists process-wide, `tests/conftest.py` no longer credits the pytest segfault to a lancedb thread the suite never starts (raven imports lancedb nowhere), and the status-bar patterns the two TUI e2e tests wait on live in one module instead of a hand-mirrored copy per file. Every new or changed assertion was checked by mutating the code under test and confirming the assertion fails. ## Type - [x] Others Test files, test configuration, and comments. The only non-test files touched are ui-tui comments and four `describe()` titles -- no behaviour change. ## Verification ``` uv run --all-extras pytest -q # 4654 passed, 25 deselected cd ui-tui && npx vitest run # 936 passed, no skips uv run --all-extras pytest tests/tui/autotest -m e2e -q # passes locally with tui-use installed and ui-tui built bash .claude/scripts/preflight_ci.sh # ALL PREFLIGHT CHECKS PASSED (including the TUI lane) ``` Mutation checks, each confirming the new assertion fails when the code under test is broken: the `session.py` nudge wiring removed; `clipToWidth` replaced with identity; `hasMeaningfulReasoning` forced to `true`; a never-sent prompt added to the multi-turn session assertion. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally ## Risk - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes No behaviour changes: the non-test diff is comments only. The renames touch git history for 19 test files; EverMind-AI#208 is the only open PR affected and has been notified. Two known items are deliberately left alone: `test_dogfood_whitelist.py` is flaky when the whole file runs (tracked in EverMind-AI#228, unchanged here), and the `lancedb_finalization_hazard` gate in `commands.py` never fires today -- its test docstring says so explicitly, so the coverage does not read as an argument for keeping it. ## Related Issues Closes EverMind-AI#223 --------- Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…erMind-AI#236) ## Summary `test_dogfood_whitelist.py` was reported as flaky: run the file and a few cases fail, a different few each time. It was not flaky. Three defects stacked, and each one hid the next. **Every one of the 14 output assertions was vacuous.** The expected pattern is matched against the whole screen, which includes the composer echoing the command just typed. 13 of the 14 were satisfied by that echo alone -- `skill list` on `skill`, `cron show` on `cron`, `sandbox list` on `sandbox`. The fourteenth, `status` (`OpenRouter|Model:`), was satisfied by the welcome frame's own status line. This is a pure string property, no timing involved. **Readiness keyed off the banner**, which paints before the app accepts input. Typing right after it, the composer was observed still showing its placeholder -- the keystrokes were dropped entirely. Defect 1 is why that was invisible: a dropped command still satisfied the assertion. **Ctrl+C is a ladder, not an exit key.** In `useInputHandlers.ts` a busy UI cancels the turn, a non-empty composer clears the input, and only then does it quit. The test pressed it once, so any command that left the UI busy timed out in `expect_exit`. That is the reported flakiness, and pressing until the process exits takes the file from `3 failed, 4 passed, 4 xfailed, 3 xpassed` to `0 failed`. The three together also explain the experiment recorded in EverMind-AI#228 as inexplicable: fixing readiness alone made one case fail *deterministically*, because the command then really submitted and really ran a turn, so the UI was busy when the single Ctrl+C arrived. Ruled out with instrumentation rather than argument: a pytest plugin logging `tui-use list` at every setup and teardown shows no session ever leaked between cases, and `tui-use info` retains `Status: exited` indefinitely, so `expect_exit` cannot lose a code it already had. ## What else the audit turned up **Three commands in the whitelist do not exist.** `skill refresh` and `skill stats` (the CLI has only `skill list` / `skill get`), and `cron show`, whose rename to `cron get` shipped long ago. All three were answered with "unknown command" and passed anyway. **The same three defects were in every other file in the tier**, so they are fixed there too, and the seven `xfail(strict=False)` markers are gone -- their reason blamed a Raven overlay-exit defect, but with a correct exit sequence all seven pass. **The README was the source.** Its reference example keyed readiness off the banner, asserted `OpenRouter|Model:`, and exited with a single Ctrl+C. Every file that copied it inherited all three. It now documents the correct sequence, states plainly what the tier cannot do, and drops eight references to documents that no longer exist. **The tier is now scoped to what only it can prove.** Dogfood covers four output shapes instead of 12 commands: `createSlashHandler.ts` routes every dispatched slash through the same `slash.exec` call, and the Python side already has 31 dedicated tests, so per-command repetition proved nothing the first case did not. `test_e2e_tui_status_slash.py` is deleted (same spawn, same command, same assertion as the dogfood `status` case) and so is `test_e2e_raven_chat_cli.py`, an xfail-strict placeholder spawning a `raven chat` that is neither implemented nor planned -- its tripwire moves to `tests/test_cli_smoke.py`, where it costs nothing and actually runs. **The Ctrl+C ladder moves into vitest.** `decideCtrlC` is now a pure function, the shape this file already used for `applyVoiceRecordResponse`, so the rungs are pinned without a PTY or a live turn. Hook behaviour is unchanged: `isTurnActive()` is still only consulted on the busy path. A static sweep of all 301 test files for assertions their own input can satisfy found exactly one more, in `test_session_fork_help`, which asserted `"fork"` against `fork --help` output whose usage line echoes the subcommand name. ## Type - [x] Others Tests, test configuration and comments, plus one behaviour-preserving extraction in `ui-tui/src/app/useInputHandlers.ts`. ## Verification ``` uv run --all-extras pytest -q # 4655 passed, 13 deselected cd ui-tui && npm test # 943 passed uv run --all-extras pytest tests/tui/autotest -m e2e # 13 passed, no xfail, no skips, 2m36s (was 22 passed / 4 xfailed / 3 xpassed in 4m21s) bash .claude/scripts/preflight_ci.sh # ALL PREFLIGHT CHECKS PASSED (including the TUI lane) ``` The dogfood file was run three consecutive times: 12 passed each, no flakes. Every new assertion was mutation-checked: restoring `skill refresh` fails on missing output; pointing a pattern at `OpenRouter` trips the idle-screen guard; spawning `sh -c 'cat > /dev/null'` makes the cat-echo assertion fail where the old one passed; registering a `chat` command trips the new tripwire; three separate mutations of the Ctrl+C ladder each turn vitest red. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally ## Risk - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes The only non-test change is extracting `decideCtrlC` out of the `useInputHandlers` Ctrl+C branch; the branch order and the guard on `isTurnActive()` are preserved, and three mutations confirm the new tests fail when it is not. Left alone deliberately: no CI job runs this tier, so these runs rest on local execution -- the repo's existing gap, now stated in the README rather than implied. The tier also still lives at `tests/tui/autotest/tests/`, a third location AGENTS.md section 5 does not sanction; splitting it into `tests/` and `tests/integration/` needs its own PR, since it is renames plus a fixture move with no bearing on this fix. ## Related Issues Closes EverMind-AI#228 --------- Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…ger (EverMind-AI#238) ## Summary `raven.cli.commands.run` hard-exited past interpreter finalization whenever lancedb's `LanceDBBackgroundEventLoop` thread was live. That thread cannot exist in this process: lancedb is a transitive dependency only, nothing under `raven/` imports it, memory talks to everos over HTTP, and everos runs out-of-process. `torch`, named alongside it in the agent-loop comment, is neither declared nor imported either. The probe returned False in every configuration, so the branch was unreachable. Removed the probe in `_exit.py`, the `if` it fed in `commands.py`, and the comment in `agent_commands.py` claiming the exit chokepoint covers that hazard. `flush_and_hard_exit` stays: `tests/conftest.py` calls it on CI, where a fully green suite was observed exiting 139 on Linux. Its module docstring now describes that caller instead of the lancedb story, and records why the gate went. Deleting the whole `except SystemExit` block is behaviour-preserving. With the probe always False it reduced to `except SystemExit: raise`, and the exit-code normalization inside it only ever fed the hard-exit call -- never the re-raised exception. ## Type - [x] Others Dead-code removal in the CLI exit path, plus the two tests that exercised the removed probe. ## Verification `CliRunner` invokes the Typer app directly and never reaches `run()`, so the console-script entry point was smoked by hand: ``` raven --version -> 0 raven skill get (missing arg) -> 2 raven nosuchcmd -> 2 raven status -> 0 unparseable ~/.raven/config.json -> one clean warning line, no traceback ``` ``` uv run --all-extras pytest -q # 4653 passed, 13 deselected (4655 before: the two probe tests are gone, # the four covering flush_and_hard_exit remain) bash .claude/scripts/preflight_ci.sh # ALL PREFLIGHT CHECKS PASSED ``` Also grepped the whole repo, including `benchmarks/` and `scripts/`, for `lancedb_finalization_hazard` and `LanceDBBackgroundEventLoop`: no references remain. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally ## Risk - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes The change is a deletion in a path no test host reaches, so the console-script smoke above is the real verification rather than the suite. If an embedded local-lancedb mode ever lands, the guard would need reinstating -- deliberately, and against a probe that can actually fire. ## Related Issues Closes EverMind-AI#232 Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…ng it (EverMind-AI#250) ## Summary AGENTS.md section 3.7 reproduced `.github/pull_request_template.md` inline, and the copy had drifted from it. Every heading differed, and the documented `Type` list had no `Refactor` or `CI / tooling` -- so a refactor PR following this file had to pick `Others`, while the real template offers the right box. The same section tells you the checkbox mirrors the commit type, which a list missing two commit types cannot do. Reference the file rather than restating it: a copy is what drifts. The filling rules now name the template's actual sections and say what each is for -- `Verification` wants the commands and their result rather than a claim that they ran, `Related Issues` distinguishes `Fixes #NNN` from a bare reference, and `Type` is a mirror of the commit type rather than a fresh judgement call. Net effect is 37 lines removed and 7 added: the rules that are not in the template stay, the duplicate of the template goes. ## Type - [ ] Fix - [ ] Feature - [x] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification ``` uv run pre-commit run --files AGENTS.md # passed make check-large-files # passed npx commitlint --from origin/main --to HEAD --config commitlint.config.cjs grep -rn "Change description|Type of change|Checklists" AGENTS.md # no matches left ``` `CLAUDE.md` is a symlink to `AGENTS.md`, so both read the corrected text. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [x] User-facing docs or screenshots are updated when needed ## Risk Documentation only; no code path changes. The advice it replaces was already unusable as written, so following the new text cannot produce a worse PR than following the old one. Rollback is a revert. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues Fixes EverMind-AI#241 Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…I#249) ## Summary Provider management used to be a whitelist: a registry of 21 vendors that decided who could be configured at all, plus a second execution path that bypassed LiteLLM entirely for knn routing. This makes LiteLLM the single execution layer and reduces the registry to what LiteLLM cannot tell us. **knn routing now runs on LiteLLM.** It needed several endpoints alive in one process, which `CustomProvider` provided by talking to the endpoints directly -- at the cost of native streaming, retries and tool-calling. `PerModelProvider` now builds one `LiteLLMProvider` per endpoint instead, and `custom_provider.py` is deleted. knn is a decision layer again; execution is LiteLLM everywhere. **Any vendor LiteLLM supports is now configurable.** A key under the vendor's name plus a `<vendor>/<model>` id is enough -- 134 vendors instead of 21, with no registry entry required. `provider list`, `raven status`, the startup gate and the TUI model picker all report them, which they previously could not: a working setup read as unconfigured, so Raven kept pushing the user into a wizard that then refused to configure that vendor. Names LiteLLM has never heard of are still rejected as typos, and a typo no longer makes Raven unstartable. **A provider now has one name.** It used to have two -- ours and LiteLLM's -- reconciled by a `litellm_prefix` field read at every site that compared a name or a model-id prefix. That field conflated two opposite facts: LiteLLM spelling the same vendor differently (`vllm` -> `hosted_vllm`) versus a provider reached through another vendor's driver (SiliconFlow over OpenAI's API). "Is this prefix mine?" has opposite answers for the two, and deriving the distinction from whether the prefix happened to be another entry's name was correct only by coincidence. LiteLLM's spelling is now the provider's own name, `litellm_prefix` is gone, and only a borrowed driver states `via_driver` -- which is absent from `route_names` by construction, so an id asking for `openai/` can no longer be answered with SiliconFlow's key. `zhipu` is renamed to `zai` on the same basis. Four facts were each implemented at several call sites and had already drifted apart -- how a model id splits into a prefix, how a name is spelled for comparison, which names refer to a provider, and how a provider's config section is found. Each now has exactly one implementation, in `registry.py` or `ProvidersConfig.get`. ### Credential routing Several paths could hand one vendor's key to another. All are fixed here: - a gateway's model prefix was read from the raw field rather than the derived one, so a gateway whose name is already LiteLLM's declared nothing and the prefix was dropped -- sending the gateway's key to the vendor named in the model id; - an explicit prefix could be overridden by a keyword match, so `deepinfra/deepseek-ai/DeepSeek-V3` put DeepInfra's key in `DEEPSEEK_API_KEY` and rewrote the model id; - a bare id naming an unconfigured vendor fell back to any credentialed direct vendor, e.g. a lone Anthropic key answering for `kimi-k2.5`; - environment variables were derived from LiteLLM's `validate_environment`, so the key was written into every variable it listed -- `AWS_SECRET_ACCESS_KEY` for Bedrock, `CLOUDFLARE_API_BASE` for Cloudflare; - `litellm.api_base` was set process-wide, leaking one provider's endpoint to every other call in the process; - the OpenRouter shortlist offered bare `anthropic/...` ids, which auto-detection reads as a request for that vendor direct, quietly leaving OpenRouter as soon as the user also held that vendor's key; - a `routing.models` entry without an `api_base` fell through to `OPENAI_BASE_URL` or `api.openai.com`, shipping the prompt and that endpoint's key to a third party; it now raises. There is no attacker model here -- these are misrouting bugs, not an authentication bypass -- but each one sends a credential somewhere its owner did not intend, so they are called out rather than folded into the summary. ### Also fixed - an empty declared section no longer answers for a provider, so credentials written under one of its other names are not shadowed by a placeholder; - differently-spelled sections (`azure-openai`, `OpenRouter`, `nanoGpt`) resolve to the same provider on both the read and the write path, so a section written as LiteLLM spells it is no longer invisible to `provider get/set` and a write no longer leaves two sections for one provider; - `_litellm_knows` returns three states instead of raising, because only one of its five call sites caught the exception; - the onboard wizard no longer drops `api_key` / `api_base` for vendors with no registry entry, and no longer overwrites a real key with `None` when a re-configure is abandoned; - `raven status` reads credentials from the loaded config, so ones supplied by environment variable are visible, and an OAuth provider without a token reports "not set" rather than a checkmark. ### New `agents.defaults.modelOverrides` lets a user override request params per model, which previously only the registry could do. knn-routed models inherit it, so a routed model behaves like the default provider. ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [x] Refactor - [ ] Other ## Verification ``` uv run --all-extras pytest -q # 4869 passed, 30 skipped, 0 failed uv run ruff check raven/ tests/ benchmarks/ uv run ruff format --check raven/ tests/ benchmarks/ ``` 79 new tests across five files. The provider-resolution suite is sweep-shaped: each invariant runs over every registered provider rather than the case that was last fixed, since the drift this PR removes was invisible to case-shaped tests. Every assertion was checked by breaking the implementation and confirming it goes red. It also carries five source guards against a second implementation reappearing. These are line scans -- tripwires, not proofs. A deliberate rewrite walks past them; what they catch is the shape that actually recurred, the same spelling copied to a new call site. Behaviour was compared against the base branch over a matrix of provider configurations, model-id shapes and forced-provider settings. Every difference falls into one of the intended changes below. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk Three user-visible behaviour changes: 1. A user whose only credentials are direct vendors -- no gateway -- and whose default model carries another vendor's prefix now gets "provider not configured" instead of a downstream 401. Anyone holding a gateway key is unaffected: a gateway legitimately routes any id. 2. The canonical section names for vLLM and Ollama become `hosted_vllm` and `ollama_chat`, LiteLLM's own spellings. Existing configs keep loading under the old names; the next `provider set` consolidates onto the new one. 3. A `routing.models` entry without an `api_base` now raises instead of silently defaulting to `api.openai.com`. Rollback is a revert: no data migration runs, and configs written before this change continue to load unchanged. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues Fixes EverMind-AI#242 Fixes EverMind-AI#243 Fixes EverMind-AI#244 Fixes EverMind-AI#245 Fixes EverMind-AI#246 --------- Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
## Summary Add a runtime-owned approval flow for protected shell commands: - Preserve unconditional deny-list behavior for commands such as recursive deletion. - Require one-shot user approval for recognized deletion commands in interactive TUI user turns. - Fail closed for non-interactive origins, expired requests, disconnects, invalid responses, and shell parsing failures. - After a recognized policy rejection or user denial, terminate the action without asking the model or a subagent to try another method. - Stop remaining sibling tool calls and further model iterations when a terminal safety decision occurs. - Recognize supported deletion commands across newlines, simple shell grouping, command substitution, common execution wrappers, and shell `-c` commands. - Match power-control commands by executable position so argument text such as `git grep shutdown` is not falsely rejected. - Use separate frontend and backend deadlines to close stale approval prompts reliably. The shell classifier is intentionally lexical. It recognizes supported command families and common shell forms, but it does not attempt to infer arbitrary side effects performed by every program or interpreter. ## Type - [ ] Fix - [x] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed Commands and results: - `uv run --frozen pytest tests/test_agent_loop_approval.py tests/test_approval_broker.py tests/test_cli_tui_commands.py tests/test_sandbox_unit.py tests/test_shell_approval.py tests/test_subagent_manager.py tests/test_tool_registry_execute.py tests/test_tui_rpc_approval.py tests/test_tui_rpc_spine.py tests/test_tui_rpc_stubs.py -q`: 255 passed. - `uv run --frozen ruff format --check raven/agent/tools/shell.py raven/agent/tools/shell_policy.py raven/agent/subagent/manager.py tests/test_shell_approval.py tests/test_subagent_manager.py`: passed. - `uv run --frozen ruff check raven/agent/tools/shell.py raven/agent/tools/shell_policy.py raven/agent/subagent/manager.py tests/test_shell_approval.py tests/test_subagent_manager.py`: passed. - `npm run type-check`: passed. - `npm run lint`: 0 errors, 22 warnings. - `npm test -- --run src/__tests__/approvalRoundTrip.test.ts`: 7 passed. No user-facing documentation or screenshots are required for this runtime and TUI safety-flow change. ## Risk - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes The sandbox keeps its existing policy bypass because it remains the containment boundary. Direct execution preserves hard-deny precedence, and approval transport failures deny execution. Interactive TUI user turns gain one-shot approval. Gateways and background origins without a trusted approval responder fail closed for recognized deletion commands. Workspace-boundary and deny-list violations remain terminal so the model cannot search for an alternative path after a safety rejection. The feature can be rolled back by reverting this pull request. ## Related Issues N/A --------- Co-authored-by: <guoqingjiang@deepglint.com>
…rMind-AI#252) ## Summary The onboarding picker offered 13 hardcoded rows while the provider layer could already reach every vendor LiteLLM routes to, so a supported provider was only configurable by hand-editing the config file. This opens the picker up and makes the model step work for everything it now offers. The picker lists all 21 registered providers in four groups (key, OAuth, local, fallback), plus a searchable second step for the vendors Raven carries no spec for. A spec is metadata, not permission: the name gate now passes those through and normalizes the spelling instead of rejecting it. Local deployments (Ollama, vLLM) are configured by address rather than by key. The old path demanded an API key with a minimum length, which nothing running on localhost has, so the wizard could not finish. The address field is seeded with whatever is already configured, falling back to the registry default. Model candidates come from one chain everywhere: what the user configured, then the curated shortlist, then LiteLLM's own catalogue. Eleven providers offered zero models before -- picking Moonshot after saving a key showed "no models listed for this provider", which reads as a rejected key. Moonshot now offers 22, Ollama 21, Copilot 27. The catalogue is read from the installed package and loaded on first use, not at import. Every model id the wizard returns carries its provider's route prefix. A bare id is routed by keyword and fallback rather than to the provider just configured, so a vendor with no spec would have been configured correctly and then called with another provider's key. "How is this provider reached" is now answered in one function rather than derived independently at thirteen call sites: a token file, an address, an endpoint plus a key, or a key alone. Each of those sites disagreeing with the others is what produced a rollback that crashed the wizard on OAuth providers, a menu that offered a key prompt to one, and a failure menu that offered to re-enter a key a local server does not have. A guard test fails if the auth flags are read anywhere else. ## Type - [ ] Fix - [x] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification ``` uv run --all-extras pytest tests/ -q -p no:randomly 4939 passed, 30 skipped, 13 deselected uv run ruff check raven/ tests/ All checks passed uv run ruff format --check unchanged ``` Every regression test added here was checked by breaking the implementation it covers and confirming it fails, in both directions where the invariant has two sides. That found one guard whose branches were unreachable in effect, which is deleted rather than pinned. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk Model candidates track LiteLLM's price table, which ships with the package, so the list moves when that dependency is upgraded rather than staying fixed. This is deliberate: pinning a local copy would have put candidates out of step with the pricing and context-window numbers read from the same table, and an earlier attempt to pin it also pinned the whole process to a table 278 entries behind. Candidates are a starting list, not a whitelist -- the field accepts free text. Providers already in a config file keep working: names are matched spelling-insensitively, and nothing rewrites existing entries. Rollback is the revert of this branch. It restores the 13-row picker; providers configured through the new rows stay in the config file and keep working, since the execution layer already supported them before this change. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues Fixes EverMind-AI#203 Fixes EverMind-AI#221 Fixes EverMind-AI#248 Partially addresses EverMind-AI#197 -- MiniMax is marked as an open-source partner in the picker, placed above the other direct providers rather than immediately after OpenRouter. The documentation note inviting other providers to collaborate is left open for someone else to pick up. --------- Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…Mind-AI#253) ## Summary Review follow-up to EverMind-AI#252. These fixes were written and verified before that PR merged but were never pushed to it, so they land here instead. Switching provider after a failed verification carried the credential flags into the next pass. Picking a local deployment then hit the guard that rejects --api-key for one, and the wizard ended on a usage error with steps 2-4 lost -- the outcome the switch loop exists to prevent. Two quieter halves of the same bug: the stale key was written to the newly picked provider with no prompt, and the stale base URL pointed it at the previous provider's machine. Three rewind sites each answered "what does a rewind discard" separately, and all three answered it the same incomplete way -- the provider flag only, while the comment claimed the failed values would not be reused. One `_rewind` answers it now, and a guard test fails if a rewind clears anything by hand. Every handler returning a picker row builds it off the event loop, instead of one warming the catalogue and three reading it inline. The read imports LiteLLM the first time, the cache is cold whenever an earlier read failed (failures are deliberately not cached), and call order belongs to the client. openai / anthropic / deepseek / gemini no longer report themselves unreachable on a healthy run: their pre-check is skipped rather than failed, so the model step printed "couldn't reach the provider" directly beneath the line saying the check had been skipped. A self-hosted endpoint is a key and the address it is sent to. Its manage-menu update re-asked only for the key, so the field that moves when the user redeploys could not be reached from that menu -- and the address it already has is offered back rather than replaced with a placeholder. Two consolidations: how LiteLLM spells a vendor was the same function in two modules and is now one, in the module that owns provider naming; and the catalogue index is read by that spelling rather than the normalized one, which would find nothing for any vendor LiteLLM hyphenates. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification ``` uv run --all-extras pytest tests/ -q -p no:randomly uv run ruff check raven/ tests/ ``` Each fix was checked by breaking it and confirming the new test fails, in both directions where the invariant has two sides. Two of those runs found more than they were aimed at: the endpoint-update fix had no test until a mutation stayed green, and writing one exposed a second defect (the stored address was not offered back); and tightening the auth-flag guard to match any receiver was confirmed against the exact spelling the old pattern missed. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk The wizard's --api-key / --base-url / --model flags now apply only to the first provider attempted. Passing them and then switching provider prompts for credentials instead of reusing the values typed for the provider that failed. Rollback is the revert of this branch; it restores the behaviour above. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues N/A -- follow-up to the review on EverMind-AI#252. Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…ck bugs (EverMind-AI#251) ## Summary `read_file` could not read images: it called `read_text(encoding="utf-8")`, so a PNG raised `UnicodeDecodeError` on byte 0. Raven has supported image *understanding* since its first commit, but only on the inbound path (an IM attachment goes into a `user` message, which every provider accepts). The path an agent needs -- looking at a file on disk -- puts the image in a **tool result**, and that is where it gets hard: OpenAI's Chat Completions types `role:"tool"` content as `string | ChatCompletionContentPartText[]`, so an image has nowhere to go at the schema level. This branch adds that capability, and fixes four pre-existing defects found on the way there. Nineteen commits, each independently reviewable; they read best oldest-first. ### Pre-existing bugs (independent of the feature) | Commit | Defect | |---|---| | `fix(utils)` | Image tokens were counted as the length of the base64 data URI. Measured on a 2.9MB PNG: **2,813,577 estimated vs ~1,300 real**, over by ~2168x, and the factor grows linearly with file size. This starves the history budget and can trip emergency shrinking on a prompt that would have fit. | | `fix(providers)` | The Responses converter ended with `json.dumps(content)` for any non-string tool result, so a multimodal result reached the model as its own base64 rendered as prose. Nothing raised. | | `fix(*)` (MCP) | Same shape, different call site: the MCP wrapper ended with `str(block)`, and `str()` on a pydantic model is its repr -- so an MCP tool returning `ImageContent` put `type='image' data='iVBORw0KGgo...'` into the prompt. Live today for any MCP server that returns a screenshot. | | `feat(agent)` (persistence) | `_save_turn` guarded truncation with `if role == "tool" and isinstance(content, str)`, and the branch below it was `elif role == "user"`. A list-shaped tool result matched neither, so it was persisted verbatim -- the 16,000-char cap never applied. | All four are the same failure mode: **a type check standing in for a capability check**. `isinstance(x, str)`, `json.dumps`, `str()` each answer "can I stringify this" when the question was "what is this" -- and they answer wrong silently as soon as the type widens. The persistence one is the only defect here that cannot be reverted: code can be changed back, bytes already written to session JSONL cannot. ### How images travel - **Detection** is by magic bytes, not extension, so a PNG named `.txt` is handled as an image and a `.png` full of text is not. - **Preprocessing** clears three independent limits, because exceeding any one gets the image rejected or silently altered: 2000px per side, 4.5MB of base64 (under the 5MB per-image cap Bedrock and Vertex AI enforce -- one payload per turn may go to any target, so it must satisfy the lowest bound), and 1568 patch tokens. Both size limits are checked rather than just the long edge: the docs are explicit that "Images larger than either limit are downscaled", so passing the pixel check alone still leaves an image the server resizes, which makes any local token estimate wrong. - **Delivery** depends on the transport, not the model. `supports_image_tool_result` returns True only for measured targets (`anthropic`, `vertex_ai`, `bedrock`) and, behind a gateway, only for measured model families (`anthropic/claude-`, `google/gemini-`); everything else takes a placeholder-plus-attachment path, the shape OpenClaw uses against Chat Completions endpoints. The list is deliberately a whitelist of what was probed rather than a blacklist of known failures, because the dangerous failure is silent: `openrouter/openai/gpt-4.1-mini` accepts the request, discards the image and confabulates a colour, while the same image in a user message gets a correct answer from it. The gateway key is a family prefix rather than the creator segment for the same reason -- `openrouter/google/gemma-3-27b-it` is served only by third-party OpenAI-compatible hosts, two of which reject the payload with a 422 while two accept it, and OpenRouter picks the host per request. Absences mean unprobed, not known-bad: `gemini` almost certainly works (LiteLLM routes it through the same transform as the listed `vertex_ai`) but there was no direct key to prove it. `ProviderSpec.image_tool_result_override` lets a deployment opt in. - **Lifetime** is one turn. Images degrade to a placeholder on the way to the session, so they are never replayed on resume or re-sent to the model in a later turn. The text kept alongside names the file path, so the model can re-read an image rather than only learning that one existed. Inbound IM images now do the same, mirroring what the non-image branch of that loop has always done for PDFs. ### Design notes for review - **`ToolOutput` stays a `str` subclass.** Its docstring already explains why -- the sentinel executor, subagent manager, context curator and tracing all put the registry's return value straight into a message or preview. `blocks` rides along as an attribute, so those four callers are untouched and need no knowledge that multimodal results exist. This makes `model_text` a contract rather than a courtesy: it is what text-only transports send and what lands in the transcript, so a tool setting `blocks` puts the metadata and the path in it, never "see the image above". - **Graceful degradation is built into the type.** A provider that ignores `blocks` still gets a self-sufficient text result naming the file, rather than nothing. - **The content-part shape is written in one place.** It was hand-written in five and re-derived in nine, with nothing verifying any of them -- a mistyped key (`imageURL`) would drop the picture with a clean log. Construction now goes through `image_block()` / `text_block()`, and a test greps the tree to keep the literal from coming back. - **TypedDicts type what Raven produces, not what it reads.** Inbound content legitimately carries parts the union does not model (an Anthropic part with `cache_control`, an MCP audio block, a future extension), and pass-through code would otherwise become a type error for behaving correctly. Read-side helpers keep taking `Any` and checking shape at runtime; a test pins that permissiveness. Worth stating plainly: CI runs no type checker, so these annotations gate nothing on merge -- they document the contract and give an editor language server something to check. - **Security posture is unchanged in kind.** `wrap_untrusted_blocks` fences text parts and leaves image bytes untouched, because a text boundary cannot contain pixels and rewriting the bytes would corrupt the picture. Instructions rendered *into* an image stay unfenced; the defense for those is the one that already covers text -- privileged actions are gated by policy regardless of what the model just looked at. This matches OpenClaw and Claude Code, neither of which tracks taint from an image to a later action. - **Pillow is pinned directly.** It already arrives via `everos[multimodal]` -> `everalgo-parser`, but that is a three-hop transitive path, so it is pinned for the same reason `tiktoken` already is. ### Deliberately not included Terminal rendering of images (needs the Kitty/iTerm2 graphics protocol and would hit the 1 MiB JSON-RPC frame cap; the goal here is images reaching the model, not the eye), a structured `details` channel for program-readable tool metadata (nothing needs it yet -- `CallToolResult.structuredContent` is the shape to copy when something does), and native PDF, audio or video input. ### Fixed during review Three defects found in review, all on the fallback path, plus one recovery mechanism added on top: | Item | Defect | |---|---| | R1 | `read_file` on a `.svg` regressed from returning the text to returning an error: the extension guess sent XML into the image branch, Pillow could not decode it, and nothing fell back to the text path. | | R2 | The image carrier message was appended inside the per-tool-call loop, so a batch whose non-last call returned an image put a `user` message between two tool results. Measured: gpt-4o returns 400, Anthropic accepts it, so it fires only on the transport that actually takes this path. | | R3 | The capability verdict was cached from `self.model` while the turn ran on `effective_model`, which the router selects per turn. A wrong `True` is the silent image loss the whitelist exists to prevent. | | R5 | The carrier message persisted as a user turn holding `[image]`, which reads on resume and in export as something the user sent. Filtered where the turn's message list is finalized, upstream of persistence, `after_turn` extraction and `backend.store` alike. | | N1 | The gateway whitelist keyed on the creator segment, admitting routes served by unmeasured third-party hosts. Narrowed to measured family prefixes. | `ErrorClassification` gained a fourth flag so a refused image is recoverable rather than fatal: classify, move the picture to a user message, retry the iteration once, cache the verdict per model. Worth stating the limit plainly -- this heals a wrong guess only when the endpoint says so. A silent drop stays undetectable, which is why the whitelist is the primary defence and the retry is the backstop. ## Type - [x] Fix - [x] Feature - [x] Refactor ## Verification Commands run: ``` uv run --extra dev ruff check raven/ tests/ # All checks passed uv run --extra dev ruff format --check raven/ tests/ uv run --extra dev pytest tests/ -q # 23 failed, 5036 passed, 30 skipped make check-commits # clean make check-large-files # clean ``` The 23 failures are pre-existing on `main` and unrelated (`test_sandbox_debug_server.py`, `test_cli_theme.py`). Verified by running the suite in a clean worktree at `origin/main` and diffing failing test IDs: **no test fails on this branch that passes on `main`**. The baseline worktree showed three extra failures in `test_channels_errors.py`, which are an artifact of `slack_sdk` being absent from that fresh environment, not a fix from this branch. 65 tests in `tests/test_read_file_image.py`, no skips, no xfail, no weakened assertions. One pre-existing test changed on purpose: `test_image_inlined_as_block` pinned the old user-content text block exactly, and now pins the new shape plus asserts the path and re-read hint are present. Every commit was checked to import cleanly on its own, since the series is meant to be read commit by commit. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [x] User-facing docs or screenshots are updated when needed Two surfaces, both updated: `read_file`'s tool description, and `TOOLS.md` (the workspace file for non-obvious tool constraints, loaded into the system prompt) which now documents content-based detection, downscaling, the one-turn lifetime, and the follow-up-message delivery path. Checked that no other doc enumerates tools -- neither README nor README.zh-CN mentions `read_file`. ## Risk - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes Backward compatibility: a tool returning `str` behaves exactly as before, and `read_file` on a text file is unchanged. `blocks` is additive and only `read_file` and the MCP wrapper set it. The registry keeps its 4-argument `add_tool_result` call for text results so no existing caller or test double sees a signature change. Rollback: reverting the branch restores prior behavior. The one asymmetry worth naming is the persistence fix -- if the block-carrying commit had landed without it, session JSONL written in between would contain base64 permanently. They are in the same commit for that reason. ## Related Issues N/A --------- Co-authored-by: zhanghui <23442919+gloryfromca@users.noreply.github.com> Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…nd-AI#258) ## Summary The onboarding wizard's EverOS memory step judged "already configured" two different ways: the role menu required a model AND an api_key, while the back-out branch after the provider picker accepted a model alone. Since the shipped everos.toml template seeds every section's model with an empty api_key, `Back` on a required role (llm / embedding) fell into `continue`, the role menu decided nothing was configured, and control dropped straight back into the picker -- forever. The bounded exit added for this case ("Give up EverOS (use native Markdown memory)") was unreachable, so Ctrl+C was the only way out of the wizard. Both call sites now share one `_everos_role_configured(section)` helper (model AND api_key), which is also the criterion `_memory_enabled()` already applied -- one definition of "configured" instead of three copies that can drift apart again. Optional roles (rerank / multimodal) were never affected: their menu offers Skip. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification - `uv run pytest tests/test_cli_onboard_commands.py tests/test_config_update_everos.py -q` -> 150 passed - `bash .claude/scripts/preflight_ci.sh` (commitlint, commit-message check, large files, pre-commit, ruff lint, focused pytest) -> all green, 5123 passed / 30 skipped - Regression gate proven to bite: reverting only the back-out branch to the old model-only check makes `test_memory_required_role_back_reaches_give_up_menu` fail with "Back re-asked the picker instead of offering an exit". - Driver against the real `_config_everos_role` with a seeded template (model set, api_key empty): before the fix `Back` re-asked the picker indefinitely; after it, the second prompt is the give-up menu and the call returns `_ABORT_EVEROS`. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk User-visible change, wizard only: `Back` on a required memory role now reaches the "pick a provider / give up EverOS" menu instead of re-asking the provider picker. A role whose everos.toml entry has a model but no api_key is now treated as unconfigured, so its menu no longer offers "Keep current" for an entry that could not work anyway. No config, schema, or runtime path changes; rollback is reverting this commit. - [ ] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues Fixes EverMind-AI#257 --------- Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…inned (EverMind-AI#256) ## Summary Selecting anything from the model picker pins its provider, so by the time someone types `/model <name>` there is almost always one pinned. A bare id matching no provider's keywords is what a vendor Raven holds no spec for looks like, and the pin was kept for it -- sending that provider's key to a different vendor, the mis-routing the prefix rules exist to prevent. The prefixed form of the same id was already handed back to auto-detection for exactly this reason; the bare form fell through. Refusing every such id would break the two cases where the pin was right: a user who configured a spec-less vendor and typed one of its model names, and a local deployment, whose server names whatever models it likes and holds no key to mis-route. So the pin is asked to justify itself instead. Its own curated list counts as evidence -- that is where a hand-added id sits, unprefixed -- and so does the catalogue; prefixes are stripped before comparing, which covers every spelling the sources use. With no evidence either way the switch is refused, naming the qualified form to type, rather than a vendor being picked on the user's behalf. An explicit provider is never second-guessed, so the picker's own path is untouched. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification ``` uv run --all-extras pytest tests/ -q -p no:randomly 5127 passed, 30 skipped, 13 deselected uv run ruff check raven/ tests/ All checks passed bash .claude/scripts/preflight_ci.sh exit 0 ``` Behaviour, driven through `config.set` end to end: | Typed | Pinned | Result | |---|---|---| | `mistral-large-latest` | `openai` | refused, config untouched | | `mistral-large-latest` | `mistral` | pin kept (the catalogue lists it) | | `some-private-tune` | `mistral`, listed in its `models` | pin kept | | `some-local-build` | `ollama_chat` | pin kept | | `mistral/mistral-large-latest` | `openai` | `auto`, unchanged | | `gpt-4o` | `mistral` | `openai`, unchanged | | any, with `provider` sent | `openai` | the sent provider, gate not reached | Each test was checked by breaking the code it covers and confirming it fails. One of those runs found that the evidence check had two branches where one sufficed -- stripping the prefix already covers the spelling the second compared exactly -- so the redundant branch is deleted rather than pinned. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk `/model <bare name>` can now be refused where it previously switched. It is refused only when nothing indicates the pinned provider serves that model, which is the case where the switch silently used the wrong credentials; the message names the qualified form to type instead. Typing a prefixed id, or picking from the picker, behaves as before. Rollback is the revert of this branch. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues N/A Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
## Summary
Two things a user hits without doing anything unusual.
**A static read started a login.** Three LiteLLM drivers ship a
device-flow
authenticator, and every entry point that resolves a model reaches it.
With no
token file on disk, reading a Copilot model's context window printed six
GitHub
device codes to stdout and blocked for 410 seconds -- two full
three-attempt login
cycles, because the bare and the openrouter-prefixed candidate reach the
same
driver. `session.create` runs that read before the first turn, so the
symptom was a
gateway that hung on opening the picker, and the agent loop runs it
again every
turn. The cost estimate, which runs after every call, hung the same way
through
`cost_per_token`.
One function now answers whether a model can be handed to LiteLLM at
all, and both
lookups consult it. It asks the installed package -- a driver either
ships
`authenticator.py` or it does not -- rather than carrying a list that
would need
regenerating on every LiteLLM bump, where a stale copy brings the hang
back for
whatever it missed.
The window lookup reads the price table before asking, which covers all
49 rows
those three drivers have between them. The ask stays for everything that
cannot
prompt, because it does more than normalize keys: for an
openrouter-prefixed
candidate it derives OpenRouter's own numbers, which are in no table
row.
**Azure OpenAI could be picked and never worked.** Its endpoint contains
the
tenant's own resource name, so there is nothing to default to, and its
client
raises on an empty `api_base`. The wizard decided who needs an endpoint
by name,
matching only the self-hosted entry, and asked Azure for a key alone --
while the
model picker kept its own list of the two providers that need one. The
same fact,
answered twice, one answer wrong. It is a registry field now and both
read it.
## Type
- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other
## Verification
```
uv run --all-extras pytest tests/ -q -p no:randomly
5049 passed, 30 skipped, 13 deselected
uv run ruff check raven/ tests/ All checks passed
bash .claude/scripts/preflight_ci.sh exit 0
```
Measured before and after on the paths that hung:
```
resolve_context_window("github_copilot/gpt-4.1") 410.59s, 6 device codes -> 128000, 2.06s
estimate_cost_usd("github_copilot/gpt-4.1") same hang -> 0.0028, 0.00s
```
Behaviour parity checked by running the old and new implementations side
by side
over the 132 model candidates Raven offers, excluding the four families
that would
hang the old one: no context window and no rate differs.
Every test added here was checked by breaking the code it covers and
confirming it
fails. Two of those runs found the test rather than the code -- one
assertion
passed no field name to the helper it was testing, and one probe raised
an
exception the lookup deliberately swallows -- and both were rewritten
until the
mutation bit.
- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed
## Risk
A model belonging to one of the three interactive-login drivers, with no
row in the
price table, now resolves to no context window instead of eventually
returning one
after the login attempts time out. The caller keeps its configured
window, which is
what it already does for any unknown model.
Picking Azure OpenAI in the wizard now asks for an endpoint and a
deployment name
in addition to the key. Existing Azure sections are untouched.
Rollback is the revert of this branch.
- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes
## Related Issues
EverMind-AI#254 -- the same assumption in the vendor step, for providers Raven
carries no spec
for. Not fixed here: that one needs somewhere to keep credentials that
are not a
single key. Azure had the same shape while being in the curated list,
which is the
half fixed here.
Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…nd-AI#259) ## Summary The wizard asks a provider which of four credential shapes it uses -- a token file, an address, an endpoint plus a key, or a key alone -- and every decision about setting it up follows from that. The model picker kept a coarser answer of its own: two shapes, plus a literal list of the two providers it knew need an endpoint. So a local deployment was reported as taking an API key and needing no address, which is exactly backwards. Ollama and vLLM could not be configured from the TUI at all: the key prompt asks for something they do not have, the address they are reached by was never asked for, and `model.save_key` required a key. The decision lives in the registry now -- where the facts it reads live -- and both sides derive from it. Three things fall out of that: * Saving a local deployment writes an empty key rather than leaving the field alone. Omitting it kept whatever was there, so a section that had once held a key would go on sending it to the user's own server. * An unconfigured provider says what it is missing -- an address, an endpoint, a key, or a sign-in -- instead of "(no key)" for all four. * An OAuth provider names `raven provider login <slug>`, not a `raven model` flow that does not exist. ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [x] Refactor - [ ] Other ## Verification ``` uv run --all-extras pytest tests/ -q -p no:randomly 5143 passed, 30 skipped, 13 deselected uv run ruff check raven/ tests/ All checks passed cd ui-tui npm run lint 0 errors npm run type-check clean npm run lint:rpc generated.ts in sync npm test 951 passed, 83 files npx prettier --check "src/**/*.{ts,tsx}" clean bash .claude/scripts/preflight_ci.sh exit 0 ``` Seven mutations were applied to check the new tests bite, five on the Python side and two on the TypeScript side; each turned the relevant test red. One of them found the code rather than the test: writing no key field for a local deployment left a stale one in place, which is why it now writes an empty one. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk Two wire changes, both reflected in `openrpc.json` and the generated types: | | Before | After | |---|---|---| | `auth_type` | `oauth` \| `api_key` | `oauth` \| `local` \| `endpoint` \| `key` | | `model.save_key.api_key` | required | optional | A TUI built before this change will not recognise the new `auth_type` values. It does not break -- the branches fall through to their default -- but a local deployment shows as needing a key rather than an address. The bundled TUI is built from this tree, so that applies only to a mismatched pair. `api_key` is still refused when empty for every shape that needs one, and now refused when present for a local deployment, which writes no key. Rollback is the revert of this branch. Provider sections already on disk are untouched. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues N/A --------- Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…verMind-AI#260) ## Summary Opening `/model` meant scrolling twenty-one rows to reach the two or three that can actually serve a model. A provider with no credentials is not something to switch to, so the first screen lists what is set up and ends with one row that opens the rest: ``` * Anthropic - 12 models * DeepSeek - 8 models + add a provider - 3 not set up ``` Both screens read the same `model.options` response -- the split is a view, and the backend is unchanged. The vendor search from the onboarding wizard is deliberately not brought over: a vendor Raven holds no spec for cannot be configured with a single key today (EverMind-AI#254). Three defects surfaced while wiring it up, two of them introduced by this change and caught by driving it rather than by reading it: * Which list the selection belongs to is held apart from which screen is showing. Deriving it from the stage meant leaving the add list for the credential screen silently re-pointed the selection at the configured list, so choosing an unconfigured provider opened the first configured one instead. * The cursor is placed by finding the current provider among the configured ones, not among all of them. An index into the full response lands past the end of the first screen's rows, and a selection past the end highlights nothing -- the picker opened with no cursor and Enter did nothing. * A local deployment can be set up here at all. The client refused to submit without an API key regardless of provider, so the one shape with no key stayed unconfigurable even after the backend stopped requiring one -- and it was shown a key field the server ignores, which reads as something to go and find first. It gets one field, labelled for the address it is reached by. ## Type - [ ] Fix - [x] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification ``` bash .claude/scripts/preflight_ci.sh exit 0 ``` That runs the full Python suite, ruff, pre-commit, and the TUI's lint, rpc-schema check, type-check, tests and build -- the same set CI runs. Five mutations were applied to check the new tests bite: dropping the filter on the first screen, deriving list membership from the stage again, demanding a key from a local deployment, showing it a key field, and locating the cursor in the full response. Each turned the relevant test red. The key-demand one passed on the first attempt, because the test checked the field labels without submitting; it now submits and asserts what `model.save_key` receives. Two assertions read field labels rather than the "Configure <name>" heading: at that depth the fake terminal's escape stripping drops the odd character from the accumulated frame, and the labels are what those screens exist for. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk Unconfigured providers are one keypress further away than before. The row that opens them states how many there are, and it is hidden when there are none. If nothing is configured the first screen holds only that row, which is the only useful thing to show; onboarding is what puts a user in that state. Rollback is the revert of this branch. No stored configuration is read or written differently. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues EverMind-AI#254 -- why the vendor search stays in the wizard rather than moving here. Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
## Summary Bump the package version from 0.1.9 to 0.1.10 (patch release). 22 PRs merged since v0.1.9, no breaking changes. Features: - EverMind-AI#260 feat(tui): list the providers that work, and the rest one level down - EverMind-AI#252 feat(*): offer every supported provider in the onboarding picker - EverMind-AI#251 feat(tools): read images with read_file, and fix four silent type-check bugs - EverMind-AI#239 feat: add shell command approval flow - EverMind-AI#217 feat: rework the TUI transcript into collapsible episodes - EverMind-AI#220 feat(cli): nudge raven upgrade in the tui status bar when behind - EverMind-AI#209 feat(providers): add MiniMax Global and CN OAuth Refactors: - EverMind-AI#259 refactor(*): one answer to which credentials a provider needs - EverMind-AI#249 refactor(providers): unify provider management on litellm The remaining PRs are fixes (EverMind-AI#255, EverMind-AI#256, EverMind-AI#258, EverMind-AI#253, EverMind-AI#238, EverMind-AI#226), test work (EverMind-AI#236, EverMind-AI#230, EverMind-AI#224), docs (EverMind-AI#250, EverMind-AI#215, EverMind-AI#200), and benchmark tooling (EverMind-AI#207). ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [x] Other ## Verification - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed Bump is limited to `pyproject.toml` and `uv.lock` (`uv lock` sync). Local preflight covers the branch CI checks (commit lint, PR title and body lint, ruff, large-file gate). ## Risk - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes Version-only change; no code or behavior change. Rollback is a revert of this commit. ## Related Issues N/A Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…d-AI#262) ## Summary Two fixes to how a release is presented, both found while cutting 0.1.10. **Install / Upgrade in the release notes.** The template shipped a single `curl | bash` line, so every release page told native Windows readers nothing about `install.ps1` and told existing users nothing about upgrading -- even though a release page is mostly read by people who already run Raven. Install now has one block per platform, mirroring README: `install.sh` for Linux / macOS / WSL2, `install.ps1` for native Windows plus the PowerShell 5.1 direct-URL note (the short URL fails there with `Permanent Redirect`). A new Upgrade section gives `raven upgrade` and its three limits: latest stable only, so a pre-release still needs the installer (rc tags are published with `--prerelease` and never become `/releases/latest`); editable checkouts are never overwritten; on native Windows an external helper finishes the job. **The draft-link trap.** GitHub addresses a draft release as `releases/tag/untagged-<hash>`, even when the tag already exists -- CI only runs after the tag is pushed. Publishing moves the release to `releases/tag/vX.Y.Z` and leaves the old URL serving its own stale page with no redirect. With 0.1.10 that draft link had been shared, so after publication two readers hit the stale page and concluded, once that the tag was never pushed and once that publishing had not taken effect; both were wrong. RELEASING.md now states this at the publish step, and the release job prints the draft URL next to the public one in its step summary, so whoever cuts the release has the correct link at hand instead of copying the address bar. The notes skeleton in RELEASING.md is also brought in line with the new template. ## Type - [ ] Fix - [ ] Feature - [x] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification - `sed "s/__VERSION__/0.1.11/g; s/__TAG__/v0.1.11/g" .github/release-notes-template.md` -> both placeholders substituted, no leftover `__` markers - Applied the same Install / Upgrade section to the pending 0.1.10 draft release and reviewed the rendered GitHub page before committing; that draft's summary, Highlights, Release Status, and Notes were left untouched - `release.yml` parsed with `yaml.safe_load`, and the modified `run:` block checked with `bash -n` - Every CI check reproduced locally, all green: `npx commitlint --from origin/main --to HEAD --config commitlint.config.cjs`, `uv run --extra dev python scripts/check_commit_messages.py origin/main..HEAD`, `uv run --extra dev python scripts/check_large_files.py origin/main..HEAD`, `uv run pre-commit run --from-ref origin/main --to-ref HEAD`, `make lint-python`, `make test-python` (5143 passed, 30 skipped), plus `scripts/check_pr_title.py` and `scripts/check_pr_body.py` on this title and body - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [x] User-facing docs or screenshots are updated when needed ## Risk Documentation, plus one added step-summary write in the release workflow. No runtime code path changes; already-published release notes are unaffected. The workflow change only adds output -- the release-creation logic is untouched, and the `else` branch (release already published) keeps its existing behaviour. Rollback is reverting the two commits. - [ ] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues N/A --------- Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…erMind-AI#265) ## Summary Five gaps a three-round review listed as follow-up. None was a defect: they were paths that work with nothing holding them down. Four are now driven end to end; the fifth turned out to be two lines that never did anything. **The retype-address branch.** A local deployment that cannot be reached is almost always a typo, so the failure menu offers the address back -- and what that choice runs had no test: it re-reads the stored address, seeds the prompt with it, writes what comes back and re-verifies. Ctrl+C there quits, as in the sibling re-enter-key branch; returning None would read as "switch provider" and roll the setup back instead of retrying. **Both flag-path guards.** A local deployment refuses `--api-key` rather than dropping it silently, and a `--base-url` with no scheme is refused rather than stored and failing at first use. **Removing a provider that serves the default model** warns first, including for a vendor Raven holds no spec for, which is decided by prefix alone. Treating "no spec" as "not the source" skipped the warning and left the default pointing at a provider whose key had just been removed. **The catalogue index.** An empty result -- a LiteLLM whose table has not loaded -- must not be cached, or every provider without a curated shortlist shows no models for the life of the process with no way to retry. Two deletions, both found by mutation rather than by reading: * The `cache_clear()` in the failure branch never had anything to clear. `lru_cache` stores only successful returns, so a raised exception re-runs on the next call by itself. The mutation that removed the line changed nothing, which is what exposed it. * The `None` check after the endpoint URL prompt was unreachable: that prompt, `_prompt_base_url`, raises on Ctrl+C rather than returning None. Its sibling `_prompt_local_api_base` does return None, which is why the two call sites that translate that None -- into an exit in the retype-address branch, into a `continue` in the manage menu -- are still live and still needed. Converging the two contracts would delete both translations; left for a separate change. Left undone deliberately: `_is_priced_template` and the borrowed-driver guard in `common_models`. Both affect candidate quality only, and the guard has no observable effect against today's catalogue. ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [x] Refactor - [ ] Other ## Verification ``` bash .claude/scripts/preflight_ci.sh exit 0 ``` Seven mutations were applied. Six turn a new test red; the seventh changed nothing, which is how the dead `cache_clear()` was found. Two of the tests were rewritten after their first version failed to bite -- one stubbed out the very `cache_clear` it was testing, and one never exercised the Ctrl+C path it claimed to cover. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk Production code shrinks by five lines, all of them unreachable or inert; the rest is tests. No behaviour changes. Rollback is the revert of this branch. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues N/A --------- Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…verMind-AI#266) ## Summary Four credential prompts in the wizard, two contracts. The key, base-URL and custom-model prompts raise on cancellation; the server-address prompt returned None. So each of its three callers had to decide what that meant, and they did not agree -- an exit in the retype-address branch, an exit in the local setup branch, a menu redraw in the manage menu. The address prompt now raises like its siblings, and all three translations are deleted rather than moved. A guard test resolves the prompt's call sites and fails if any of their results is tested for None, so it holds under a rename. Scope is those four prompts, not the module: the vendor search and the provider row picker also return None on cancellation and their caller translates it. That is a separate chain, left alone here rather than claimed. ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [x] Refactor - [ ] Other ## Verification ``` make lint-python && make test-python ruff check / format --check clean 5151 passed, 30 skipped, 13 deselected cd ui-tui && npm run lint && npm run lint:rpc && npm run type-check && npm test 0 errors; generated.ts in sync; 951 passed ``` Mutations, all behaving as intended: returning None from the prompt again turns the contract test red; re-adding a translation at any of the three call sites turns the guard red, under either variable name those sites use; and an unrelated `base_url is None` in a function that never calls the prompt leaves it green. The first version of the guard failed the second of those and would have failed the third. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk Cancelling the server-address prompt in the manage menu now leaves the wizard instead of returning to the menu -- the same thing Ctrl+C does at every other prompt in it. That is the behaviour change; it is the point of the change. Production code shrinks by seven lines (+3 / -10). Rollback is the revert of this branch. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues N/A --------- Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
## Summary
Adds Hermes as a cold-start import source, alongside the existing Claude
Code
scanner, across three kinds of data.
**Memory files.** `MEMORY.md` and `USER.md` are discovered through the
same
profile resolution Hermes itself uses (`HERMES_HOME`, then
`active_profile`
redirecting to `<root>/profiles/<name>`). `USER.md` entries are
additionally
mirrored into the native `user_memory/profile/user.md`: six host
consumers
(Curator, Personalizer, four Sentinel producers) read that file directly
and
never see EverOS-only content. An LLM picks each entry's H2 heading;
every
failure path lands the entry under `## Notes`, which injection always
includes,
so a misclassification costs tokens rather than visibility.
**Conversations.** Sessions are enumerated by shelling out to
`hermes sessions export --dry-run`, whose listing is capped at 100 rows.
The
scanner walks time windows until every session is covered rather than
silently
importing the first 100. Only ended sessions are candidates, and
`reopen_session()` can clear `ended_at`, so the window reconciliation
documents
the resulting race in both directions instead of assuming monotonicity.
**Skills.** Provenance is classified from on-disk evidence -- the
`.bundled_manifest` name-to-md5 map, `.hub/lock.json`, and `.usage.json`
-- with
a `package_hash` byte-identical to Hermes' own `_dir_hash`, so a skill
still
matching its factory content is left alone and only user-authored or
curator-managed skills are copied into the local skill pool. Skills
never travel
as ScanResults, so the menus, the confirm line and the final summary all
state
the skill count explicitly; without it a run that installs 12 of 82
reads as
having dropped 70.
Failure isolation: `scan_all` now reports a failing scanner through an
`on_error` callback and keeps the other platforms' results, and a Hermes
run
whose conversations cannot be enumerated still returns its memory files.
Two pre-existing defects found while working here are fixed in the same
branch:
a multi-line recall hit escaped its bullet in the context segment
renderer, and
a recalled EverOS profile was rendered as a Python repr and left
uncapped.
Also in this branch, from hands-on testing of the finished flow:
- `raven import run` could never reach its interactive path. The
selectors
called questionary's synchronous `ask()` from inside `asyncio.run()`,
and
prompt_toolkit drives `Application.run()` through `asyncio.run()`, which
raises inside a running loop. No test covered it: every case passed
`--platform` and `--tier`, bypassing the selectors.
- Import outcomes now render as one block after the progress display
closes.
Printing them from `_build_and_run` sent them to this module's Console
while
onboard's live progress belongs to its themed Console, and Rich's Live
can
only hold back writes on its own console, so they landed mid-bar and
left the
bar duplicated.
- The `import run` selectors use the shared prompt style and glyphs
instead of
questionary's defaults; onboard's foreground import gains the phase
reporter
it never had, so the multi-second USER.md mirror is no longer silent
behind a
bar reading 100%.
The EverOS backend moves with it. Agent-track recall turned out to be
dead:
agent-track HYBRID fuses `agent_case` and `agent_skill` through a rerank
cross-encoder, and with no rerank provider the server refuses the
request
outright, which `recall` catches and turns into an empty list against a
file-only logger. A live 1.1.3 probe returns `RuntimeError:
owner_type='agent'
with method='hybrid' requires a rerank provider`. The adapter now reads
`capabilities.rerank` from `/health` and asks for the LLM lane only when
the
cross-encoder is genuinely absent -- always sending it would make every
correctly configured user pay an LLM call per recall, never sending it
leaves
the track dead for anyone who skipped the optional rerank role in
onboard.
That capability is only readable from everos 1.2.1, so the pin moves
1.1.3 ->
1.2.1, skipping 1.2.0 (built from a branch missing the 1.1.4 fixes,
including a
knowledge-upload path traversal). Memory endpoints move to `/api/v2`,
canonical
since 1.2.0; `/api/v1` still resolves to the same handlers but is
documented as
a legacy alias. Only everos endpoints move -- the Skill Hub and
OpenRouter
clients have their own v1 surfaces.
That upgrade also broke an assumption Raven was making. "Server running"
and
"server can recall" used to be the same statement; 1.2.1 boots with
`[llm]` alone
instead of aborting, so a misconfigured provider answers 200 and quietly
serves
less. Raven decided a role was configured by reading model + api_key out
of
`everos.toml`, which cannot see that. Capabilities are now read from
`/health` at
all three points where it matters: the memory step (right after the
server it
already starts), `raven doctor` (which had no EverOS check at all), and
`backend.start()` -- the only one of the three on the path every session
takes,
which is where an expired key actually surfaces. It follows the
precedent already
set there for native Windows and prints to stderr rather than a log
nobody reads,
but does not drop to a no-op: writes still land, and `everos cascade
backfill`
gives those rows their vectors once the provider is fixed.
The same reading makes the embedding role genuinely optional. It was not
before:
recall sends no `method`, so it took the server default of HYBRID, which
everos
refuses without an embedding provider -- skipping the role produced a
memory that
stored everything and returned nothing. The adapter now asks for KEYWORD
when no
embedding capability is reported, searching the same rows lexically
instead of
semantically, and that also rules out the rerank fallback because
KEYWORD's agent
path never reaches the cross-encoder. So the wizard stops requiring
embedding and
states what each role buys before asking for it -- llm alone gets
keyword recall,
embedding adds meaning, rerank sharpens ordering -- and skipping
embedding is
reported in colour rather than dim, because skipping rerank costs
ordering while
skipping embedding costs semantic recall entirely.
Fault and degradation are now distinguished rather than lumped together:
only a
missing llm sets doctor's exit code, and `raven doctor` names what each
unconfigured optional role costs, per role, since "optional" alone does
not help
anyone decide. The wizard also stops asking whether to enable memory at
all --
everos is the only backend, so the question framed a choice that does
not exist,
and both of its answers were described wrongly (declining was said to
use "native
Markdown memory", which does not exist: `raven/plugin/memory/` holds
only everos,
and `ContextAssembler.owns_compaction` is a hardcoded True that stops
the
consolidator from ever updating user.md). Backing out is still possible
and now
states the real consequence in colour. The memory LLM field is
pre-filled with
the user's own main model rather than a recommended id, because a
recommendation
is only reachable if their key carries it.
Two rounds of review on this branch found defects worth naming, since
each was
invisible for the same reason -- no test covered the path:
- **Skills were copied before the run's own `Proceed?` gate.** The two
early
returns that install a skills-only import sit above it, so a machine
whose only
importable data is skills had directories copied before being asked
anything.
Both paths now name the count and ask; the wizard's `Start?` gate is
further
down still, so its two call sites ask as well. Every covering test
passed
`--yes`, which is what hid it.
- **A cancelled run still ran both post-phases.** `run_import` returns
normally
once it sees the cancel file, so `raven import stop` halted the
conversation
loop and then started an LLM call per USER.md entry plus a full skill
copy.
- **Org-mirror skills were imported past a gate Hermes applies itself.**
Hermes
token-gates `_org/`: with no `.active_org` marker it prunes the whole
mirror,
and with one it descends only into the named org, so a stale mirror is
content
Hermes no longer loads. Discovery now mirrors that. Excluding `_org`
outright
would have been the opposite error -- it drops the active org's skills.
- **`doctor` and the wizard probed the default address.** Both took
`DEFAULT_EVEROS_BASE_URL` while the backend reads the configured
`base_url`, so
a user who moved everos off port 18791 was told it was not running -- by
the
checks added to make a degraded server visible.
- **An unbuilt multimodal role was reported by nobody.** The
section-to-capability
mapping had an entry both consumers skipped.
- **The USER.md progress line reached 100% before the last call.** Each
report
counted the call about to start; on a measured 9.4s run that left ~3s
sitting at
100%, the exact symptom the phase reporter was added to remove.
- **Registry-name collisions were only checked within one run**, so a
name an
earlier import or the user's own skill already claimed produced the
invisible
skill that `get(name)` never returns.
- **"All platforms" plus "run in background" could not work.** `import
run` has no
all-platforms flag, so the child reached its platform picker with
DEVNULL on
both streams and no terminal, after the wizard reported the import as
started.
It now runs in the foreground and says so.
The e2e tests this branch made dead are fixed too: identity moved to the
host, so
`config={"agent_id": ...}` is ignored, and the agent-track test wrote
under the
services default while querying the tag -- `hits` was empty, its per-hit
assertions vacuous, and an `xfail` absorbed it. The plugin manifest
moves to
1.1.0, which the upgrade SOP in this same diff asks for.
Not in scope: Codex, KimiCode and OpenClaw remain placeholders;
cold-start
import stays an EverOS-only path with no native fallback.
## Type
- [x] Feature
- [ ] Fix
## Verification
```
uv run --all-extras pytest -q 5405 passed, 30 skipped, 13 deselected
uv run ruff check . All checks passed!
uv run ruff format --check . 873 files already formatted
make check-large-files passed
```
Beyond the suite, the importer was run against a real Hermes install at
`--tier full`: memory files, conversations and skills all landed, EverOS
extracted agent cases from the imported conversations
(`quality_score=1.0` in
the server log), and a second run over the same data reported
`Submitted: 0, Skipped: 4`, confirming the checkpointing is idempotent.
The everos upgrade was verified against a live server holding real
imported
data: on 1.1.3 the agent track returns 500 and `/api/v2` returns 404; on
1.2.1
with this change the agent track returns an `agent_case` and the user
track is
unchanged at three episodes. Re-verified after killing the server and
cold
starting it, with the resolved binary printed to confirm which build
answered.
`raven doctor` and the wizard both report llm and embedding available
with
rerank absent against that server.
The suite was also run with HOME pointed at an empty directory, which is
how the
memory-probe and import-step tests were caught reading the developer's
own
configuration instead of their own fixtures. That pass also found the
test suite
waiting out `ensure_everos_server`'s 30s readiness timeout on every
machine
without a running server -- always, on CI -- which cost one file 34 of
its 35
seconds and is now stubbed.
The interactive flow was verified under a pty, comparing the two console
wirings directly, and each new assertion was mutation-tested: every
behavioural
change on this branch had its fix reverted and the covering test
confirmed red,
and only that test. Where a fix had a plausible wrong shape, that shape
was
mutated too -- excluding `_org` wholesale rather than token-gating it,
reporting
progress after the await instead of before, seeding the registry-name
check from
the pool without ordering it after the target resolution. Two of those
mutations
survived and were the more useful result: one showed a guard clause
could not
fire (`_target_for` only ever returns a path that does not exist, so
excluding it
from the pool lookup was dead code, now removed), the other showed the
wizard's
own confirmation sits below the returns it was supposed to cover.
- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
User-facing docs are unchanged: this adds a platform to an existing
documented
command rather than a new surface. The developer docs move with the code
--
`docs/memory-plugin-architecture.md` (the everos pin, and the upgrade
SOP now
stating that a one-way migration belongs in the release notes) and
`scripts/README.md` (two deleted scripts).
## Risk
- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes
The import writes to EverOS and to `user_memory/profile/user.md`, and
copies
skill directories into the local skill pool. EverOS has no delete
endpoint, so
an unwanted import is not undone by reverting this branch; the skill
copies and
the profile edits are plain files and can be removed by hand. Import
state
lives in `~/.raven/import_state.json` and can be deleted to start over.
Behaviour changes for existing users: the onboard wizard's import step
and
`raven import run` both look different (shared prompt style, skill
counts,
one summary block). `raven import run`'s interactive path changes from
"crashes immediately" to working. Rolling back restores the crash.
Three more, from the review round: a skills-only import now asks before
copying
(one extra prompt, skippable with `--yes`), an all-platforms import
chosen with
"run in background" runs in the foreground instead and says so, and
skills under
a stale `_org/` mirror are no longer imported -- Hermes does not load
them
either, but anyone who had them imported by an earlier build keeps them,
since
nothing here deletes.
Two consequences of the everos upgrade that operators need to know:
- **The index migration is one-way.** everos 1.2.1 migrates the LanceDB
schema
on first start and prunes older manifest versions, so once a machine has
run
this, its memory index can no longer be read by an older everos.
Reverting
the branch does not undo it.
- **A missing provider no longer aborts startup.** 1.2.1 boots with
`[llm]`
alone. A misconfigured embedding provider used to fail loudly; it now
degrades
to keyword search, and rows written in that state carry no vector until
`everos cascade backfill` is run. everos itself only logs one warning
line,
which is why the wizard, `raven doctor` and `backend.start()` all
surface it --
the last of those runs every session, so an expired key is reported
rather
than discovered.
- **Behaviour change for users who skip embedding.** Recall degrades to
keyword
matching instead of failing, and the wizard no longer stops them from
getting
there. That is the point of the change, but it means a user can now end
up with
a working memory that is measurably worse at recall than the previous
all-or-nothing setup would have allowed.
## Related Issues
N/A
---------
Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…ros parser (EverMind-AI#273) ## Summary `understand_media` has been failing on every call. EverOS moved multimodal LLM resolution into `aparse_file` and dropped the `llm` keyword from `enrich_content_items`, so the call raised `TypeError`. The per-item `except Exception` then degraded that into a per-file `[could not understand: ...]` note, so an interface mismatch read like an attachment the parser could not handle. Three changes: - Drop the stale `llm=` keyword. The up-front `get_multimodal_llm_client()` probe stays: the parser resolves its own client per file, so without it an unconfigured deployment reports the same failure once per attachment. - Re-raise a `TypeError` from the parser as a batch-level `MultimodalUnavailableError`. A signature mismatch is true of every attachment, so per-item degradation is the wrong shape for it. - Steer image reads to `read_file` in the tool description. Both tools claimed images, so the model could pick the one that hands back another model's transcription instead of the picture itself. This matches what `render.build_user_content` already tells the model next to each attachment. The existing tests stub `understand_files` wholesale and never reach everos, which is why the drift was invisible. `TestEverosParserContract` stubs only the two external boundaries -- the optional parser extra's availability probe and the vision call -- and lets the real `enrich_content_items` run, so the next signature change fails a test instead of a user. It covers the failure contract as a whole: a signature mismatch aborts the batch; an unsupported modality, an LLM error and a missing file are each reported per item without stopping the rest; and an unconfigured client or a missing parser extra fails the call once, before any file is parsed. That exposed a pre-existing isolation bug in the same test file: the autouse `_cleanup_modules` fixture evicted every module a test imported, including lancedb, whose Rust extension panics when re-imported after a logger is installed. It now clears only the fakes it injects. Fixing the `pre-commit diff` job then surfaced a second version skew. `.pre-commit-config.yaml` pins ruff v0.15.12 while the dev group resolved 0.15.11, and the two format the same signatures differently, so `uv run ruff format --check` passed locally on files CI rejected. The dev group is now pinned to `ruff==0.15.12`. A range does not fix this: local ruff only has to differ from the hook's version, in either direction, for the two to disagree. The published `dev` extra keeps its looser `ruff>=0.6.0`, since that is for consumers installing `raven[dev]` rather than what `uv run ruff` resolves. Not covered here: `read_file` on a PDF still ends at `Error reading file: 'utf-8' codec can't decode byte ...` with no pointer to `understand_media`, and `render.build_user_content` names `understand_media` unconditionally even where the plugin is not registered. Both need a way for core code to refer to an optional tool without hardcoding its name, which is worth designing separately. ## Type - [x] Fix ## Verification - `uv run pytest tests/ --ignore=tests/integration` -> 1 failed, 5176 passed, 40 skipped. The failure is `tests/test_cli_theme.py::test_bold_accent_renders_styled_not_bare`, which fails identically on an unmodified checkout (the local terminal reports 16-color rather than truecolor) and passes in the `unit` job here. - `uv run pytest tests/test_plugin_tools.py` -> 36 passed. - `uv run ruff format .` -> 873 files left unchanged, and `uv run ruff check .` -> All checks passed, both on the newly pinned 0.15.12. - `uv run pre-commit run` over this branch's diff -> every hook Passed or Skipped, with the working tree unmodified afterwards. This is the same command the `pre-commit diff` job runs. - The new tests were confirmed red before the fix, reporting `enrich_content_items() got an unexpected keyword argument 'llm'` -- the same string users were seeing. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally ## Risk - `understand_media` goes from always-failing to working, so attachments that used to produce "could not understand" now spend vision-model calls and tokens. It still requires `EVEROS_MULTIMODAL__*`; an unconfigured deployment gets one clear error instead of one per file. - A `TypeError` anywhere under the parser now aborts the batch rather than degrading a single file. That is the intent; per-item isolation is unchanged for every other exception. - The dev ruff pin is exact, so it has to be bumped together with the rev in `.pre-commit-config.yaml`. Moving one without the other puts local formatting and CI back out of step. - No schema, config, or on-disk format changes. Rollback is a revert of these commits. - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues N/A --------- Co-authored-by: zhanghui <23442919+gloryfromca@users.noreply.github.com> Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
## Summary
`/model` listed the four OAuth providers and did nothing when Enter was
pressed on
one. Enter now hands the terminal to `raven provider login <family>` and
lands on
that provider's model list when the sign-in succeeds. Everything the fix
walked
through on the way is here too, because a working key was not enough to
reach a
model:
**Where a credential lives.** Each OAuth family resolved its own path --
Codex
through `oauth_cli_kit`, Copilot under `~/.config/litellm`, MiniMax
under
platformdirs -- and the code that wrote one was not always the code that
read it,
so a completed sign-in could report itself unauthenticated forever. One
function
now answers where a credential lives, under `~/.raven/oauth`, and
"configured"
means the file parses as a credential rather than that a path exists.
LiteLLM's
driver owns the Codex flow end to end, which retires the `oauth-cli-kit`
dependency.
**Signing in.** `provider login` is now the driver's own flow, which is
what makes
the credential it writes the one the request path reads. Two hazards
come with
handing the flow over, and both are closed here rather than left for a
user to
find: asking LiteLLM for a token falls through to starting a device flow
when a
refresh fails, so a `model.options` RPC or a `provider test` could print
a device
code and poll for fifteen minutes with nobody waiting to type it -- both
entry
points are refused on the instance that only reads. And the driver
treats its own
unfinished-attempt timestamp as a flow in progress, so an interrupted
sign-in would
make the next one wait five silent minutes; the login path discards it.
Signing in
while already signed in says so instead of announcing a flow it skips, a
revoked
credential falls through to a real sign-in, and the failure text names
both reasons
a refresh can fail -- LiteLLM wraps a revoked token and an unreachable
network in
one error, so claiming either one would send half the users to fix the
wrong thing.
**Which Codex models exist.** Two ids were hard-coded, in the registry
and in the
provider constructor, and a live account refused both with "not
supported when
using Codex with a ChatGPT account". Which slugs an account may use is
only
knowable by asking it, so the account's own catalogue answers -- cached
per
credential, with failures cached too so being offline costs one timeout
rather than
one per keystroke, and never asked while signed out.
**Probes.** `provider test` fetched a ChatGPT token for every
non-MiniMax OAuth
family, so Copilot was probed with somebody else's credential. Each
family is
dispatched explicitly now -- and one with no check written yet says so,
rather than
borrowing the nearest one. Codex and Copilot each get a probe that asks
the way
their backend accepts being asked: Codex's account catalogue, and for
Copilot the
endpoint out of its own API key file with the editor headers its driver
sends,
because the generic `{api_base}/v1/models` request could reach neither
-- for
Copilot it answered "api_base is empty" without asking, and asking would
have had
a valid seat refused. A refused credential reads as a credential problem
and an
unreachable host as a network fault.
Not verified: that GitHub answers 200 to the Copilot request. That needs
a live
seat; what is pinned by test is the request that goes out.
**Cost.** A call on a subscription was costed at $0.00,
indistinguishable from a
free model. Plan billing is declared in the registry, "no per-token
price" is a
distinct answer from zero, the boot banner no longer opens a plan-billed
session at
$0.00, and the invented subscription prices are gone.
**The wizard and the terminal.** The wizard's model step defaults to the
newest
model the account offers, and says which one it used when the answer was
empty --
Codex has no static default to fall back on any more, and a substitution
nobody
mentions is how a test message ends up sent with a model the user did
not pick.
Three more: importing LiteLLM printed over the TUI's rendering,
`RAVEN_TUI_DISABLE_MOUSE` was turned back on a moment after start, and
the TUI
spawned whatever `raven` came first on `PATH` rather than the one that
started the
session -- which is how a sign-in writes a credential the running
process will
never read.
## Type
- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other
## Verification
```
make lint-python ruff check: all checks passed; ruff format --check: clean
make lint-tui eslint + lint:rpc + tsc --noEmit: exit 0 (pre-existing warnings only)
make test-tui Test Files 85 passed (85), Tests 969 passed (969)
make test-python 5525 passed, 48 skipped, 13 deselected, 2 warnings in 187.32s
```
The suite was reading the developer's real OAuth credentials before this
branch:
`import_litellm` publishes the token-directory variables process-wide
and they
outlive the test that triggered the import, so a later test that faked
`$HOME`
still resolved a live credential -- which made providers report
themselves
configured inside a sandboxed home and sent the Codex catalogue lookup
to the
network. On a signed-in machine that was a red suite; it is green now,
and the
variables point at a temp directory for every test.
Review on this branch kept turning up tests that passed either way, so
the guards
were re-checked by mutation: break the implementation in an isolated
worktree, run
the test, confirm it goes red.
- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed -- nothing
under
`docs/` or `README.md` names a credential location or the wizard's
startup
condition, so there was nothing to update
## Risk
Five changes are visible to someone who upgrades:
1. **Existing OAuth sign-ins are not migrated.** Credentials are read
from
`~/.raven/oauth` now, so a signed-in user signs in once more. The old
files are
left where they are, untouched.
2. **`~/.raven/oauth` is created 0700, and an existing one is tightened
to it.** A
user who widened that directory on purpose will find it narrowed; the
files in it
are credentials, and the drivers that write them do not set a mode
themselves.
3. **`provider login` over a credential that already works reports and
exits**
instead of re-running the browser flow. `raven provider reset <name>` is
the way
to sign in as somebody else, and the command says so.
4. **Startup now notices a default model whose provider has no
credentials**, which
it did not before: it asks the config which provider serves the model
rather than
accepting any configured key as proof. The answer is a notice naming the
model
and pointing at `/model` -- not the wizard, which would restart at the
language
screen to fix one line. The wizard still runs for a config with nothing
usable.
5. **Plan-billed providers stop reporting `$0.00`** and are left out of
the session
total rather than adding zero to it.
Removed: a five-second `config.get {key: 'mtime'}` poll in the TUI.
`config.get`
answers with its four whitelisted keys whatever it is handed, so `mtime`
was never
in the response, the poll always took its early return, and the MCP
reload it
guarded never ran -- while costing a round trip on the same stdio pipe
that carries
`prompt.submit`. `/reload-mcp` is unaffected.
Rollback is a revert of the merge: the credential move is a read-path
change, so
reverting reads the old locations again and a user signed in before the
upgrade is
signed in after the revert.
- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes
## Related Issues
Fixes EverMind-AI#263, EverMind-AI#212, EverMind-AI#218, EverMind-AI#269, EverMind-AI#275, EverMind-AI#276, EverMind-AI#277, EverMind-AI#278
---------
Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…rMind-AI#283) ## Summary GitHub pairs one closing keyword with one reference. `Fixes #NNN, #MMM` closes the first and merely mentions the second. PR EverMind-AI#279 wrote seven issues after a single keyword and left six of them open after merge; they were closed by hand. Two one-line changes state the rule where a description actually gets written: - `.github/pull_request_template.md` - the `Related Issues` hint. - `AGENTS.md`, section 3.7 - the `Related Issues` filling rule. A third drops `Closes EverMind-AI#123` from the commit-footer list in section 3.1. That line was wrong on its own terms: section 3.3 already records that this repo squash-merges and drops individual commit bodies, so a closing keyword in a commit footer never reaches `main` and closes nothing. The template hint also stops naming `EverMind-AI#123`, which is a live open issue here, and a placeholder should not be a real number. ## Type - [ ] Fix - [ ] Feature - [x] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification Documentation only; no code paths change. ``` uv run pre-commit run --from-ref origin/main --to-ref HEAD -> all hooks Passed or Skipped make lint-python -> ruff check / format --check clean make test-python -> 5525 passed, 48 skipped, 13 deselected in 271.05s npx commitlint --from origin/main --to HEAD --config commitlint.config.cjs -> OK PYTHONPATH=. uv run python scripts/check_commit_messages.py origin/main..HEAD -> OK PYTHONPATH=. uv run python scripts/check_large_files.py origin/main..HEAD -> OK grep -nP "[^\x00-\x7F]" .github/pull_request_template.md -> 0 matches. The template is embedded into every PR description, which CI lints as ASCII-only, so it has to stay ASCII. ``` - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [x] User-facing docs or screenshots are updated when needed ## Risk No behaviour change. The visible effect is a slightly longer hint under `Related Issues` when a new PR opens. Rollback is a revert of this branch. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues N/A --------- Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…AI#282) ## Summary A live `/model` switch rebuilt the provider but only reassigned `loop.provider` / `loop.model`. `AgentLoop.__init__` had already handed that provider to the subagent manager, the context engine's LLM-backed segments and the memory consolidator, and each kept its own reference. Switching away from a dead credential fixed the main loop while subagents and the skill rewriter/gate went on authenticating against the endpoint the user had just abandoned. Cron was a fourth victim: its runs failed with the same 401 and its history rendered the failures as blank rows. `AgentLoop.set_provider` now fans the pair out to every holder it built, and the context engine walks its builders duck-typed so a text-only segment is skipped rather than raising. ### In-flight work Every LLM call site reads the provider off `self` at call time, so an unconditional swap relays one conversation across two vendors. How that surfaces depends on the path: the `chat_with_retry` sites turn a rejected request into `finish_reason="error"` content, so the turn reports a failure with no sign that its endpoint moved, while `_llm_call_stream` -- the path a TUI turn takes -- catches only `TimeoutError` and lets the rejection propagate. Neither is a diagnosis the user can act on. Two mechanisms, because the two lifetimes differ. Both are superseded by EverMind-AI#284, which makes the model a property of the conversation and gets the same guarantee from the context copy that `asyncio` makes at task creation -- if the two land together, the park described here exists only between the two merges. - **The loop parks.** A switch arriving while any turn runs is held and adopted at the next `run_turn` entry. One boundary covers eight `self.provider` reads in `loop/main.py` plus the context engine and consolidator underneath them; a snapshot would have to be threaded through each. The park is a depth counter, not a flag: `OriginPools` gates USER and system origins on independent semaphores with no global cap, and the TUI defaults to one slot each, so a user turn and a cron turn overlap on one loop. Both ends gate on zero, and the last turn out adopts so a park cannot outlive the turns it waited on. - **Subagents snapshot.** A spawn is a detached task that outlives the turn, so the park cannot reach it. `spawn` captures the pair it was asked for and passes it down; capturing later would miss the window where a spawn waits on the concurrency gate and a sandbox boot. This is the second line of defence, not the first. `tui_rpc.methods.config` already rejects a switch outright when the caller's own session has a turn in flight; the park covers what that guard cannot see -- a caller that passes no `session_id`, and proactive turns running in their own lanes. Note the RPC still answers `applied: True` and the config file is already written, so a parked switch is applied on disk while the loop reports the old model until the last turn drains. ### Also here - `curator_model` is re-derived on a switch with the constructor's own expression, so the same config cannot mean one thing at build time and another after. The default is non-empty, so in practice it is a pin; an explicitly empty `context.curator_model` follows the agent model, and now follows it in both places. - The concrete no-op `set_provider` on the context-engine ABC is concrete so a future implementation with no LLM-backed segment is not forced to write an empty override. `ContextAssembler` is the only one today and does override it. ### Scope `AgentLoop` and the subsystems it builds. `HeartbeatService` and the Sentinel stack take the same provider but are siblings on the gateway side, which registers no tui_rpc methods, so `loop.set_provider` cannot and does not reach them. Not reachable today; worth an issue if the two sides ever converge. `MemoryConsolidator` is re-pointed but its detached consolidation tasks are not snapshotted -- a single call rather than a multi-turn conversation, so the split-conversation argument does not apply, but it is the same shape. ## Type - [x] Fix ## Verification ``` uv run pytest tests/ -q 5302 passed, 1 failed uv run ruff check raven/ tests/ # All checks passed uv run ruff format raven/ tests/ # unchanged ``` That failure is not this branch. `tests/test_cli_theme.py::test_bold_accent_renders_styled_not_bare` fails the same way on an unmodified `main` at `53aeb0c` when the whole file runs (verified in a detached worktree) and passes when the single test runs alone; it is a `COLORTERM` artifact and CI is green on it. `tests/test_default_context_engine.py::TestTwoTrackConcurrency::test_skill_and_memory_run_concurrently` also failed in some runs of this branch and of unrelated ones -- a timing assertion that flakes under full-suite load, passing alone and with its own file. A clean re-run at this head has only the theme failure. A later review round found one of these mutations still surviving -- moving the spawn snapshot into `_run_subagent_inner` -- because neither existing test could see the window it exists for: one stubbed `_run_subagent` wholesale (proving `spawn` passes a pair, not when the pair is read) and the other called `_run_subagent_inner` directly, bypassing `spawn`, the concurrency gate and the sandbox boot. There is now a test that holds the gate shut, switches the provider while the task sits in that window, releases it, and asserts which provider actually served the call. The same round found two docstrings scoped wider than the code: the mid-turn split does not raise on the `chat_with_retry` sites but does on `_llm_call_stream`, which is the path a TUI turn takes; and the context-engine ABC's concrete no-op was justified by a reason an abstract method would satisfy equally. Both corrected. The tests here were rebuilt after a review found the previous set only exercised the dispatcher -- replacing any receiver's `set_provider` with `pass` left it green. They now build a real `AgentLoop` and assert the gate, rewriter, curator, curator assembler, history trimmer, subagent manager and consolidator all moved; guard the attribute names the fan-out walks against a rename; and drive the real `run_turn` and the real `_run_subagent_inner`. Verified by mutation -- each of these turns something red: | Mutation | Result | |---|---| | `SubagentManager.set_provider` -> `pass` | 3 failed | | `CuratorSegmentBuilder.set_provider` -> `pass` | 1 failed | | rename the `subagents` attribute the fan-out reaches | 2 failed | | delete the `finally` that releases the turn slot | 3 failed | | adopt on `run_turn` entry unconditionally | 1 failed | | never park | 2 failed | | spawn without the snapshot | 1 failed | | curator `set_provider` drops the re-derive | 1 failed | | move the spawn snapshot into `_run_subagent_inner` | 1 failed | - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes `_run_subagent` / `_run_subagent_inner` take the provider and model as parameters now; the two suites that stub them are updated. No public API changes. Rollback is a revert -- the previous behaviour is the 401. ## Related Issues N/A --------- Co-authored-by: arelchan <204152633+arelchan@users.noreply.github.com> Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…verMind-AI#285) ## Summary Before this change a picture a user attached never reached the model. `TurnRequest.media` was wired end to end and channels already used it, but `turn.send` had no `media` field, so a front end could only paste the path into the message text. This adds that field and the delivery rules behind it. **Getting the file in.** `turn.send` takes an optional `media` array of workspace paths, resolved with the filesystem tools' own policy (so `uploads/shot.png` means the same thing it means to every other tool). A path that does not resolve, resolves outside the allowed directory, or cannot be read costs its own note in the message rather than the whole turn. The list is bounded at 64 in the schema, and one message inlines at most 16 images or 16 MB of base64. **Deciding how it reaches the model.** A vision-capable model gets the image inlined as a base64 `image_url` block in the user message; anything else gets a text note naming the file, and the note names a description tool only when one is actually registered. The image is preprocessed by the same `prepare_image` that `read_file` already used: a 4032x3024 phone photo goes out as 1180x885, which is 30x less base64 than inlining it raw. **Where the verdict comes from.** The gateway catalog Raven already fetches and caches for pricing, which publishes `input_modalities` for every model it lists. That completeness is why it is the source rather than LiteLLM's price table, which states `supports_vision` on under a third of its rows -- reading that silence as a denial would take a picture that reaches Grok, Llama 4 and the Qwen-VL family today and replace it with prose. The catalog is read from cache only, never fetched inside a turn, and warmed on a background thread with a cooldown. The warm is not optional: the pricing path asks LiteLLM first and only reaches this catalog when that table misses, which excludes every model Raven ships a default for, so without it the probe would answer optimistically forever on a fresh install. **Which way it fails.** Absence, staleness, a lookup that raises, and a caller-chosen deployment name all read as "no answer", which resolves to optimism and is never cached. Being wrong that way fails loudly at the endpoint. Being wrong the other way is silent: the picture never arrives and the model answers from the surrounding text as though it had seen one, and unlike the tool-result path there is no automatic recovery. Deployment names are excluded for that reason -- `gpt-4` is the name Azure's own quickstarts use, and a team keeps the deployment name while repointing it at a newer model, so joining it against a vendor catalog answers about somebody else's model. ## Type - [ ] Fix - [x] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification - `uv run pytest tests -q` -> 1 failed, 5334 passed, 58 skipped - `LITELLM_LOCAL_MODEL_COST_MAP=true uv run pytest tests -q` -> 3 failed, 5332 passed, 58 skipped - `uv run ruff check raven tests` -> clean; `uv run ruff format --check raven tests` -> 791 files already formatted - `npm run lint:rpc` -> generated.ts in sync; `npm run type-check` -> clean; `npm test` -> 969 passed - All 4 failures above reproduce on a pristine `main` worktree and are unrelated to this change: `test_cli_theme.py::test_bold_accent_renders_styled_not_bare` is a `rich` version issue, the two `test_token_wise_pricing.py` MiniMax-M3 assertions need a cost map the bundled LiteLLM does not carry, and `test_default_context_engine.py::TestTwoTrackConcurrency::test_skill_and_memory_run_concurrently` is a load-sensitive timing assertion. - Verified against a live catalog that the probe denies the five text-only models Raven ships defaults for and grants every vision model tested, while every deployment and local-runtime name stays optimistic. - Verified on a cold cache that the first turn answers optimistically without blocking, the background warm lands, and the next turn on the same long-lived loop gets the real verdict. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes `media` is optional, so every existing caller is unaffected, and the channels producer of `TurnRequest.media` is untouched. Attachment paths are resolved with the same policy as the file tools, so `restrict_to_workspace` still bounds them and a path outside it is dropped. Pricing is deliberately unchanged: the catalog entry gained an `input_modalities` field and the on-disk cache version was bumped, but the pricing lookup itself has no deleted or modified lines, and cost and context-window resolution were compared against `main` across 40 real model ids in both cost-map modes with identical results. Rollback is `ProviderSpec.vision_override` per provider for a wrong verdict, and reverting the commit otherwise. **Known scope boundary.** Channels populate `TurnRequest.media` today and benefit immediately. The TUI does not reach this lane yet, and not for want of a client: it already implements both attachment gestures (a dropped path via `image.attach`, a clipboard image via `clipboard.paste`), but the server side of those is a `not supported in v0.1` stub or absent entirely, and the TUI catches the failure silently. Connecting them is a follow-up PR, and it is server-side work -- `ui-tui/` needs no change. Two follow-ups deliberately left out of scope: a `media` list that fails validation surfaces as `-32603 internal_error` rather than `-32602`, which is how every `TurnSendParams` validation failure already behaves and needs a dispatcher change to fix; and the vision verdict is computed for the routed primary while a LiteLLM fallback further down the chain is sent the same message list, which the sibling transport probe shares and which needs the chain assembled per candidate. ## Related Issues N/A --------- Co-authored-by: arelchan <204152633+arelchan@users.noreply.github.com> Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…ind-AI#287) ## Summary Closes out the provider path in one PR: the provider module redesign, every open issue and backlog item that lives on the identity / connection / routing / capability axes, and multi-endpoint failover as a new feature. 78 commits, one problem per commit, rebased onto current main. Redesign (base): four provider decisions were implemented outside `raven/providers/` and had drifted copies -- wire form, credential grammar, pin resolution, price/window ladder, cache dialect. Each now has one owning module the surfaces call. Fixes on that base, most visible first: - The context window walks one ladder (explicit config > the model's real window > documented fallback). Previously the config default 65536 fed trimming and budgets raw, so a 200k-window model lost two thirds of its context every turn; an unresolvable window now renders the gauge's empty state instead of a number that is nobody's. Construction and /model switches resolve without touching the network, and a switch parked behind a running turn re-resolves the window at adoption, not at the RPC call. - A provider without real streaming (azure, codex) no longer renders upstream errors as normal assistant text: the terminal stream delta carries the classification and joins the same recovery path the non-streaming call uses. Both classify their non-200 from the live status code (shared ProviderHTTPError) instead of regex-guessing the rendered text, which also removes the bare "404" substring match that misfiled a 400 whose body happened to embed one. - Codex SSE failures keep their structured code, so an overloaded backend is retried instead of classified unknown. - Orphan `</think>` recovery (backends launched without their reasoning parser) is gated to the backend shapes that produce it, so ordinary content mentioning the tag is never cut. - Fallback hops are vetoed when both identities are certain and disagree (previously a cross-vendor hop went out under the wrong key, or silently to the wrong backend on a shared model name); the knn path dispatches each hop to its own endpoint and inherits model_overrides through the rotor. - User model_overrides win over shipped extra_body defaults, and the merge no longer drops user keys behind a gateway. - The credential gate, the reader and the builder answer one way: a spec's shipped default address satisfies the gate exactly when the reader would serve it (custom runs on a bare key again, azure still demands its address), endpoint entries inherit the flat api_base/extra_headers per field, and every display face -- provider list, endpoint list, the TUI picker -- reports the same resolved view, secrets redacted (extra_headers values included, on the flat section field too). - The vision probe stops joining operator-chosen names against the vendor catalog: it reads through the TUI's lazy proxy to see the Azure transport, treats an explicit-selection gateway (custom) as caller-chosen, and the background catalog warm is no longer suppressed for the life of the process by a stale on-disk table. - Skill forge rewriter/gate follow the configured agent model (the only auxiliary LLM calls that did not). - The onboarding wizard refuses the six litellm vendors a bare API key cannot configure, with the actual requirement named, instead of writing a section that 401s forever. - The OAuth handoff replays a signal swallowed mid-login, so a SIGHUP no longer leaves a headless TUI. New feature -- multi-endpoint failover (several accounts on one vendor): - `providers.<name>.endpoints` (label / apiKey / apiBase / extraHeaders) with `endpointStrategy: sticky | round_robin`; the three credential spellings (explicit list, Gemini api_key_list, flat fields) resolve through one reader with strict precedence and no key merging. - `EndpointRotorProvider` rotates and fails over with per-endpoint cooldown (30s doubling to 300s, process-local state); auth failures rotate -- another account's key is exactly what a dead key needs -- while endpoint-agnostic failures return immediately. Streams rotate only before the first delta, so tokens are never replayed. - Managed from `raven provider endpoint add|remove|list` and the TUI model picker; the session footer names the active endpoint. Write faces refuse a keyless endpoint for key-credential providers (local deployments keep their legitimate keyless shape); invalid sections fail loudly instead of being read as empty and overwritten. - Verified live twice: the rotor directly, and the full stack from a config file through make_provider (a dead key 401s, cools, fails over, answers). The first live run caught the auth-rotation gap the mocks could not. Also in this PR: the onboarding wizard split (5069 -> 3000 lines, pure moves with every migrated monkeypatch target mutation-checked), CONTEXT.md terms (Provider Endpoint, window ladder), benchmark alignment (pinchbench now prices through the shared ladder instead of a private drifted copy), and the model picker reads the config twice per open instead of twice per provider row. Reviewed adversarially across three external rounds and four internal panel rounds on two model families: 24 before-merge findings raised in total, every one either fixed with a mutation-verified test or refuted with executed evidence (one reviewer finding was withdrawn after a 945-case main-parity sweep); final verdicts RATIFY. The rebase also adopted the review notes left on EverMind-AI#282/EverMind-AI#285 that landed on this code: image-capability verdicts are invalidated on a provider switch, and a parked switch logs its park and its adoption. Known follow-ups, named in the review thread (issues to follow): the fallback routing loop contradicts the registry's explicit-selection note for `custom` (pre-existing on main), the remaining bare status substrings in classify_error (429/5xx, pre-existing), per-hop identity rebuilding for fallback chains, and the pre-existing SessionInfo shape mismatch. ## Type - [x] Feature - [ ] Fix - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification - `uv run pytest tests/ -q` -- 6128 passed, 33 skipped, run after the rebase onto current main; the provider-path test files additionally re-run under an empty HOME with identical results. - `make lint-python` clean; commit messages pass commitlint and scripts/check_commit_messages.py across all 78 commits. - `cd ui-tui && npm run lint && npx tsc --noEmit && npx vitest run` -- 86 files, 982 tests passed; `npm run gen:rpc -- --check` in sync. - Live probes (OpenRouter, tiny max_tokens): rotor failover and full-stack assembly failover, both passing; logs kept locally. - Fixes are pinned by deletion mutations that turn a named test red; review rounds ran 22 such mutations and the two survivors were themselves fixed (one dead guard deleted, one vacuous test replaced). ## Risk - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes User-visible behavior changes: `contextWindowTokens` unset now resolves to the model's real window (explicit values are fully respected and no longer overridden); the context gauge shows an empty state when the window is unknown; azure/codex upstream errors surface as errors instead of assistant text; the wizard refuses six key-only-unconfigurable vendors with the real requirement named; `custom` with only an apiKey starts again (as on main) and the picker no longer demands an address the gate does not; key-credential providers refuse keyless endpoints at write time and `endpoint add --api-key` becomes optional for local deployments; endpoint keys and extra_headers values are redacted in `provider get`/`list` and over RPC; a model switch logs when it parks behind a running turn and when it adopts. Rollback is a straight revert of the squash commit; no data migration is involved (config additions are opt-in fields). ## Related Issues Fixes EverMind-AI#124, fixes EverMind-AI#234, fixes EverMind-AI#155, fixes EverMind-AI#152, fixes EverMind-AI#254, fixes EverMind-AI#151, fixes EverMind-AI#143, fixes EverMind-AI#144, fixes EverMind-AI#197. References EverMind-AI#281 (not reproducible on current or reported code; the api_key forwarding it suspected is now pinned by regression tests), EverMind-AI#119 (already fixed by EverMind-AI#116; remaining item is the installer redirect, not provider code). --------- Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
## Summary Bump version from 0.1.10 to 0.1.11 for the next release. Only pyproject.toml and uv.lock change. ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [x] Other ## Verification - `uv lock` - resolved 193 packages, updated raven v0.1.10 -> v0.1.11 - `uv run python scripts/check_commit_messages.py origin/main..HEAD` - exit 0 - `git diff --stat origin/main..HEAD` - 2 files changed, 2 insertions(+), 2 deletions(-) - [ ] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk - [ ] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes Version-only bump; no code changes. Rollback: revert the squash commit. ## Related Issues N/A Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
connect_mcp_servers called list_tools() once and registered only the first page, so any MCP server that paginates tool discovery silently exposed a partial tool surface. Follow nextCursor page to page until the server stops returning one, guarding against a repeated cursor trapping discovery in a loop. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Member
This PR was auto-closed by a history rewrite, not by a maintainerOn 2026-09-12 the Your work is safe in your fork. Nothing in your repository was modified. To restore this PR, rebase onto the new git remote add upstream https://github.com/EverMind-AI/Raven.git # if you don't have it
git fetch upstream 'refs/tags/backup/pre-linearize-20260912:refs/tags/old-main'
git fetch upstream main
git rebase --onto upstream/main $(git merge-base <your-branch> old-main) <your-branch>
git push --force-with-leaseThen open a new PR and reference this one. Thank you for the contribution, and sorry for the disruption. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
connect_mcp_servers called list_tools() once and registered only the tools on the first page. A server that paginates tool discovery (returns a nextCursor) silently exposed a partial tool surface: every tool past the first page was omitted with no error.
Follow nextCursor page to page until the server stops returning one, then register the full set. Track seen cursors so a misbehaving server that re-issues the same cursor cannot trap discovery in a loop.
The paging loop lives in a new _collect_tools helper so it is unit-testable without standing up a transport/registry.
Type
Verification
uv run pytest tests/test_mcp_tools_pagination.py -v-> 3 passeduv run ruff format(2 files unchanged),uv run ruff check(all passed)Risk
Related Issues
Fixes #301